django-slack-tools 0.1.0__py3-none-any.whl → 0.2.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 +1 -1
- django_slack_tools/app_settings.py +29 -4
- django_slack_tools/slack_messages/admin/message.py +1 -1
- django_slack_tools/slack_messages/apps.py +9 -0
- django_slack_tools/slack_messages/backends/__init__.py +2 -2
- django_slack_tools/slack_messages/backends/base.py +182 -47
- django_slack_tools/slack_messages/backends/dummy.py +8 -9
- django_slack_tools/slack_messages/backends/slack.py +29 -91
- django_slack_tools/slack_messages/message.py +40 -58
- django_slack_tools/slack_messages/migrations/0001_initial.py +0 -2
- django_slack_tools/slack_messages/migrations/0002_default_policy.py +35 -0
- django_slack_tools/slack_messages/migrations/0003_default_recipient.py +35 -0
- django_slack_tools/slack_messages/migrations/0004_slackmessagingpolicy_template_type.py +23 -0
- django_slack_tools/slack_messages/models/messaging_policy.py +29 -3
- django_slack_tools/slack_messages/tasks.py +112 -0
- django_slack_tools/utils/slack/django.py +8 -24
- django_slack_tools/utils/slack/message.py +74 -25
- django_slack_tools/utils/template/__init__.py +5 -0
- django_slack_tools/utils/template/base.py +17 -0
- django_slack_tools/utils/template/dict.py +52 -0
- django_slack_tools/utils/template/django.py +140 -0
- django_slack_tools/views.py +1 -1
- {django_slack_tools-0.1.0.dist-info → django_slack_tools-0.2.0.dist-info}/METADATA +39 -21
- django_slack_tools-0.2.0.dist-info/RECORD +43 -0
- {django_slack_tools-0.1.0.dist-info → django_slack_tools-0.2.0.dist-info}/WHEEL +1 -1
- django_slack_tools/utils/dict_template.py +0 -55
- django_slack_tools-0.1.0.dist-info/RECORD +0 -36
- {django_slack_tools-0.1.0.dist-info → django_slack_tools-0.2.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -2,29 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import logging
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
from django_slack_tools.app_settings import app_settings
|
|
9
|
-
from django_slack_tools.
|
|
9
|
+
from django_slack_tools.slack_messages.models.message_recipient import SlackMessageRecipient
|
|
10
10
|
from django_slack_tools.utils.slack import MessageBody, MessageHeader
|
|
11
11
|
|
|
12
12
|
from .models import SlackMessagingPolicy
|
|
13
13
|
|
|
14
|
-
logger = getLogger(__name__)
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
|
-
from .
|
|
17
|
+
from django_slack_tools.slack_messages.backends.base import BaseBackend
|
|
18
18
|
|
|
19
|
+
from .models import SlackMessage
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
def slack_message( # noqa: PLR0913
|
|
21
23
|
body: str | MessageBody | dict[str, Any],
|
|
22
24
|
*,
|
|
23
25
|
channel: str,
|
|
24
26
|
header: MessageHeader | dict[str, Any] | None = None,
|
|
25
27
|
raise_exception: bool = False,
|
|
26
28
|
get_permalink: bool = False,
|
|
27
|
-
|
|
29
|
+
backend: BaseBackend = app_settings.backend,
|
|
30
|
+
) -> SlackMessage:
|
|
28
31
|
"""Send a simple text message.
|
|
29
32
|
|
|
30
33
|
Args:
|
|
@@ -33,96 +36,75 @@ def slack_message(
|
|
|
33
36
|
header: Slack message control header.
|
|
34
37
|
raise_exception: Whether to re-raise caught exception while sending messages.
|
|
35
38
|
get_permalink: Try to get the message permalink via extraneous Slack API calls.
|
|
39
|
+
backend: Messaging backend. If not set, use `app_settings.backend`.
|
|
36
40
|
|
|
37
41
|
Returns:
|
|
38
42
|
Sent message instance or `None`.
|
|
39
43
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
body = MessageBody.from_any(body)
|
|
45
|
+
header = MessageHeader.from_any(header)
|
|
46
|
+
message = backend.prepare_message(channel=channel, header=header, body=body)
|
|
43
47
|
|
|
44
|
-
return
|
|
45
|
-
channel=channel,
|
|
46
|
-
header=header,
|
|
47
|
-
body=body,
|
|
48
|
-
raise_exception=raise_exception,
|
|
49
|
-
get_permalink=get_permalink,
|
|
50
|
-
)
|
|
48
|
+
return backend.send_message(message, raise_exception=raise_exception, get_permalink=get_permalink)
|
|
51
49
|
|
|
52
50
|
|
|
53
51
|
def slack_message_via_policy( # noqa: PLR0913
|
|
54
|
-
policy: str | SlackMessagingPolicy,
|
|
52
|
+
policy: str | SlackMessagingPolicy = app_settings.default_policy_code,
|
|
55
53
|
*,
|
|
56
54
|
header: MessageHeader | dict[str, Any] | None = None,
|
|
57
55
|
raise_exception: bool = False,
|
|
58
56
|
lazy: bool = False,
|
|
59
57
|
get_permalink: bool = False,
|
|
60
58
|
context: dict[str, Any] | None = None,
|
|
61
|
-
|
|
59
|
+
backend: BaseBackend = app_settings.backend,
|
|
60
|
+
) -> int:
|
|
62
61
|
"""Send a simple text message.
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
Some default context variables are populated and available for use in templates.
|
|
64
|
+
See corresponding backend implementation for available context variables.
|
|
66
65
|
|
|
67
66
|
Args:
|
|
68
|
-
policy: Messaging policy code or policy instance.
|
|
67
|
+
policy: Messaging policy code or policy instance. Defaults to app's default policy.
|
|
69
68
|
header: Slack message control header.
|
|
70
69
|
raise_exception: Whether to re-raise caught exception while sending messages.
|
|
71
70
|
lazy: Decide whether try create policy with disabled, if not exists.
|
|
72
71
|
get_permalink: Try to get the message permalink via extraneous Slack API calls.
|
|
73
72
|
context: Dictionary to pass to template for rendering.
|
|
73
|
+
backend: Messaging backend. If not set, use `app_settings.backend`.
|
|
74
74
|
|
|
75
75
|
Returns:
|
|
76
|
-
|
|
76
|
+
Count of messages sent successfully.
|
|
77
77
|
|
|
78
78
|
Raises:
|
|
79
79
|
SlackMessagingPolicy.DoesNotExist: Policy for given code does not exists.
|
|
80
80
|
"""
|
|
81
81
|
if isinstance(policy, str):
|
|
82
82
|
if lazy:
|
|
83
|
-
policy, created = SlackMessagingPolicy.objects.get_or_create(
|
|
83
|
+
policy, created = SlackMessagingPolicy.objects.get_or_create(
|
|
84
|
+
code=policy,
|
|
85
|
+
defaults={
|
|
86
|
+
"enabled": app_settings.lazy_policy_enabled,
|
|
87
|
+
"template": app_settings.default_template,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
84
90
|
if created:
|
|
91
|
+
# Add default recipient for created policy
|
|
92
|
+
recipient = SlackMessageRecipient.objects.get(alias=app_settings.default_recipient)
|
|
93
|
+
policy.recipients.add(recipient)
|
|
85
94
|
logger.warning("Policy for code %r created because `lazy` is set.", policy)
|
|
86
95
|
else:
|
|
87
96
|
policy = SlackMessagingPolicy.objects.get(code=policy)
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
return []
|
|
91
|
-
|
|
92
|
-
header = MessageHeader.model_validate(header or {})
|
|
98
|
+
header = MessageHeader.from_any(header)
|
|
93
99
|
context = context or {}
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
overridden_reserved = {"mentions", "mentions_as_str"} & set(context.keys())
|
|
98
|
-
if overridden_reserved:
|
|
101
|
+
messages = backend.prepare_messages_from_policy(policy, header=header, context=context)
|
|
102
|
+
if not policy.enabled:
|
|
99
103
|
logger.warning(
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
messages: list[SlackMessage | None] = []
|
|
106
|
-
for recipient in policy.recipients.all():
|
|
107
|
-
# Auto-generated reserved kwargs
|
|
108
|
-
mentions: list[SlackMention] = list(recipient.mentions.all())
|
|
109
|
-
mentions_as_str = ", ".join(mention.mention for mention in mentions)
|
|
110
|
-
|
|
111
|
-
# Prepare rendering arguments
|
|
112
|
-
kwargs = {"mentions": mentions, "mentions_as_str": mentions_as_str}
|
|
113
|
-
kwargs.update(context)
|
|
114
|
-
|
|
115
|
-
# Render and send message
|
|
116
|
-
rendered = render(template, **kwargs)
|
|
117
|
-
body = MessageBody.model_validate(rendered)
|
|
118
|
-
message = app_settings.backend.send_message(
|
|
119
|
-
policy=policy,
|
|
120
|
-
channel=recipient.channel,
|
|
121
|
-
header=header,
|
|
122
|
-
body=body,
|
|
123
|
-
raise_exception=raise_exception,
|
|
124
|
-
get_permalink=get_permalink,
|
|
104
|
+
"Created %d messages but not sending because policy %s is not enabled.",
|
|
105
|
+
len(messages),
|
|
106
|
+
policy.code,
|
|
125
107
|
)
|
|
126
|
-
|
|
108
|
+
return 0
|
|
127
109
|
|
|
128
|
-
return messages
|
|
110
|
+
return backend.send_messages(*messages, raise_exception=raise_exception, get_permalink=get_permalink)
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import django.db.models.deletion
|
|
4
4
|
from django.db import migrations, models
|
|
5
5
|
|
|
6
|
-
import django_slack_tools.utils.dict_template
|
|
7
6
|
import django_slack_tools.utils.slack.django
|
|
8
7
|
|
|
9
8
|
|
|
@@ -163,7 +162,6 @@ class Migration(migrations.Migration):
|
|
|
163
162
|
blank=True,
|
|
164
163
|
help_text="Dictionary-based template object.",
|
|
165
164
|
null=True,
|
|
166
|
-
validators=[django_slack_tools.utils.dict_template.dict_template_validator],
|
|
167
165
|
verbose_name="Message template object",
|
|
168
166
|
),
|
|
169
167
|
),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Generated by Django 4.2.11 on 2024-04-09 13:39
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from django.db import migrations
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
10
|
+
from django.db.migrations.state import StateApps
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _create_default_policy(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
14
|
+
db_alias = schema_editor.connection.alias
|
|
15
|
+
model_type = apps.get_model("slack_messages", "SlackMessagingPolicy")
|
|
16
|
+
model_type.objects.using(db_alias).create(code="DEFAULT", enabled=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _delete_default_policy(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
20
|
+
db_alias = schema_editor.connection.alias
|
|
21
|
+
model_type = apps.get_model("slack_messages", "SlackMessagingPolicy")
|
|
22
|
+
model_type.objects.using(db_alias).filter(code="DEFAULT").delete()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Migration(migrations.Migration):
|
|
26
|
+
dependencies = [
|
|
27
|
+
("slack_messages", "0001_initial"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
operations = [
|
|
31
|
+
migrations.RunPython(
|
|
32
|
+
code=_create_default_policy,
|
|
33
|
+
reverse_code=_delete_default_policy,
|
|
34
|
+
),
|
|
35
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Generated by Django 4.2.11 on 2024-04-09 13:39
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from django.db import migrations
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
10
|
+
from django.db.migrations.state import StateApps
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _create_default_recipient(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
14
|
+
db_alias = schema_editor.connection.alias
|
|
15
|
+
model_type = apps.get_model("slack_messages", "SlackMessageRecipient")
|
|
16
|
+
model_type.objects.using(db_alias).create(alias="DEFAULT")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _delete_default_recipient(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
|
|
20
|
+
db_alias = schema_editor.connection.alias
|
|
21
|
+
model_type = apps.get_model("slack_messages", "SlackMessageRecipient")
|
|
22
|
+
model_type.objects.using(db_alias).filter(alias="DEFAULT").delete()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Migration(migrations.Migration):
|
|
26
|
+
dependencies = [
|
|
27
|
+
("slack_messages", "0002_default_policy"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
operations = [
|
|
31
|
+
migrations.RunPython(
|
|
32
|
+
code=_create_default_recipient,
|
|
33
|
+
reverse_code=_delete_default_recipient,
|
|
34
|
+
),
|
|
35
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 4.2.16 on 2024-10-19 05:07
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("slack_messages", "0003_default_recipient"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AddField(
|
|
13
|
+
model_name="slackmessagingpolicy",
|
|
14
|
+
name="template_type",
|
|
15
|
+
field=models.CharField(
|
|
16
|
+
choices=[("D", "Dictionary"), ("DJ", "Django"), ("DI", "Django Inline"), ("?", "Unknown")],
|
|
17
|
+
default="D",
|
|
18
|
+
help_text="Type of message template.",
|
|
19
|
+
max_length=2,
|
|
20
|
+
verbose_name="Template type",
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
5
7
|
from django.db import models
|
|
6
8
|
from django.utils.translation import gettext_lazy as _
|
|
7
9
|
|
|
8
|
-
from django_slack_tools.utils.dict_template import dict_template_validator
|
|
9
10
|
from django_slack_tools.utils.model_mixins import TimestampMixin
|
|
10
11
|
from django_slack_tools.utils.slack import header_validator
|
|
11
12
|
|
|
12
13
|
from .message_recipient import SlackMessageRecipient
|
|
13
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
14
18
|
|
|
15
19
|
class SlackMessagingPolicyManager(models.Manager["SlackMessagingPolicy"]):
|
|
16
20
|
"""Manager for Slack messaging policies."""
|
|
@@ -19,6 +23,21 @@ class SlackMessagingPolicyManager(models.Manager["SlackMessagingPolicy"]):
|
|
|
19
23
|
class SlackMessagingPolicy(TimestampMixin, models.Model):
|
|
20
24
|
"""An Slack messaging policy which determines message content and those who receive messages."""
|
|
21
25
|
|
|
26
|
+
class TemplateType(models.TextChoices):
|
|
27
|
+
"""Possible template types."""
|
|
28
|
+
|
|
29
|
+
DICT = "D", _("Dictionary")
|
|
30
|
+
"Dictionary-based template."
|
|
31
|
+
|
|
32
|
+
DJANGO = "DJ", _("Django")
|
|
33
|
+
"Django XML-based template."
|
|
34
|
+
|
|
35
|
+
DJANGO_INLINE = "DI", _("Django Inline")
|
|
36
|
+
"Django inline template."
|
|
37
|
+
|
|
38
|
+
UNKNOWN = "?", _("Unknown")
|
|
39
|
+
"Unknown template type."
|
|
40
|
+
|
|
22
41
|
code = models.CharField(
|
|
23
42
|
verbose_name=_("Code"),
|
|
24
43
|
help_text=_("Unique message code for lookup, mostly by human."),
|
|
@@ -42,13 +61,20 @@ class SlackMessagingPolicy(TimestampMixin, models.Model):
|
|
|
42
61
|
blank=True,
|
|
43
62
|
default=dict,
|
|
44
63
|
)
|
|
45
|
-
|
|
64
|
+
template_type = models.CharField(
|
|
65
|
+
verbose_name=_("Template type"),
|
|
66
|
+
help_text=_("Type of message template."),
|
|
67
|
+
max_length=2,
|
|
68
|
+
choices=TemplateType.choices,
|
|
69
|
+
default=TemplateType.DICT,
|
|
70
|
+
)
|
|
71
|
+
template: models.JSONField[Any] = models.JSONField(
|
|
46
72
|
verbose_name=_("Message template object"),
|
|
47
73
|
help_text=_("Dictionary-based template object."),
|
|
48
|
-
validators=[dict_template_validator],
|
|
49
74
|
null=True,
|
|
50
75
|
blank=True,
|
|
51
76
|
)
|
|
77
|
+
|
|
52
78
|
# Type is too obvious but due to limits...
|
|
53
79
|
objects: SlackMessagingPolicyManager = SlackMessagingPolicyManager()
|
|
54
80
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Celery utils."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from celery import shared_task
|
|
9
|
+
from celery.utils.log import get_task_logger
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
|
|
12
|
+
from django_slack_tools.slack_messages import message
|
|
13
|
+
from django_slack_tools.slack_messages.models import SlackMessage
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
logger = get_task_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@shared_task
|
|
22
|
+
def slack_message(
|
|
23
|
+
body: str | dict[str, Any],
|
|
24
|
+
*,
|
|
25
|
+
channel: str,
|
|
26
|
+
header: dict[str, Any] | None = None,
|
|
27
|
+
raise_exception: bool = False,
|
|
28
|
+
get_permalink: bool = False,
|
|
29
|
+
) -> int | None:
|
|
30
|
+
"""Celery task wrapper for `message.slack_message`.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
body: Message content, simple message or full request body.
|
|
34
|
+
channel: Channel to send message.
|
|
35
|
+
header: Slack message control header.
|
|
36
|
+
raise_exception: Whether to re-raise caught exception while sending messages.
|
|
37
|
+
get_permalink: Try to get the message permalink via extraneous Slack API calls.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
ID of sent message.
|
|
41
|
+
"""
|
|
42
|
+
sent_msg = message.slack_message(
|
|
43
|
+
body,
|
|
44
|
+
channel=channel,
|
|
45
|
+
header=header,
|
|
46
|
+
raise_exception=raise_exception,
|
|
47
|
+
get_permalink=get_permalink,
|
|
48
|
+
)
|
|
49
|
+
return sent_msg.id
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@shared_task
|
|
53
|
+
def slack_message_via_policy( # noqa: PLR0913
|
|
54
|
+
policy: str,
|
|
55
|
+
*,
|
|
56
|
+
header: dict[str, Any] | None = None,
|
|
57
|
+
raise_exception: bool = False,
|
|
58
|
+
lazy: bool = False,
|
|
59
|
+
get_permalink: bool = False,
|
|
60
|
+
context: dict[str, Any] | None = None,
|
|
61
|
+
) -> int:
|
|
62
|
+
"""Celery task wrapper for `message.slack_message_via_policy`.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
policy: Messaging policy code.
|
|
66
|
+
header: Slack message control header.
|
|
67
|
+
raise_exception: Whether to re-raise caught exception while sending messages.
|
|
68
|
+
lazy: Decide whether try to create policy with disabled, if not exists.
|
|
69
|
+
get_permalink: Try to get the message permalink via extraneous Slack API calls.
|
|
70
|
+
context: Context variables for message rendering.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Number of sent messages.
|
|
74
|
+
"""
|
|
75
|
+
return message.slack_message_via_policy(
|
|
76
|
+
policy,
|
|
77
|
+
header=header,
|
|
78
|
+
raise_exception=raise_exception,
|
|
79
|
+
lazy=lazy,
|
|
80
|
+
get_permalink=get_permalink,
|
|
81
|
+
context=context,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@shared_task
|
|
86
|
+
def cleanup_old_messages(
|
|
87
|
+
*,
|
|
88
|
+
base_ts: str | None = None,
|
|
89
|
+
threshold_seconds: int | None = 7 * 24 * 60 * 60, # 7 days
|
|
90
|
+
) -> int:
|
|
91
|
+
"""Delete old messages created before given threshold.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
threshold_seconds: Threshold seconds. Defaults to 7 days.
|
|
95
|
+
base_ts: Base timestamp to calculate the threshold, in ISO format. If falsy, current timestamp will be used.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Number of deleted messages.
|
|
99
|
+
"""
|
|
100
|
+
dt = datetime.fromisoformat(base_ts) if base_ts else timezone.localtime()
|
|
101
|
+
|
|
102
|
+
if threshold_seconds is None:
|
|
103
|
+
logger.warning("Threshold seconds not provided, skipping cleanup.")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
cleanup_threshold = dt - timedelta(seconds=threshold_seconds)
|
|
107
|
+
logger.debug("Cleaning up messages older than %s.", cleanup_threshold)
|
|
108
|
+
|
|
109
|
+
num_deleted, _ = SlackMessage.objects.filter(created__lt=cleanup_threshold).delete()
|
|
110
|
+
logger.info("Deleted %d old messages.", num_deleted)
|
|
111
|
+
|
|
112
|
+
return num_deleted
|
|
@@ -2,44 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import pydantic
|
|
8
5
|
from django.core.exceptions import ValidationError
|
|
9
|
-
from django.utils.translation import gettext_lazy as _
|
|
10
6
|
|
|
11
7
|
from .message import MessageBody, MessageHeader
|
|
12
8
|
|
|
13
9
|
|
|
14
|
-
def header_validator(
|
|
10
|
+
def header_validator(d: dict) -> None:
|
|
15
11
|
"""Validate given value is valid message header."""
|
|
16
12
|
try:
|
|
17
|
-
MessageHeader.
|
|
18
|
-
except
|
|
13
|
+
MessageHeader.from_any(d)
|
|
14
|
+
except Exception as exc:
|
|
19
15
|
err = _convert_errors(exc)
|
|
20
16
|
raise err from exc
|
|
21
17
|
|
|
22
18
|
|
|
23
|
-
def body_validator(
|
|
19
|
+
def body_validator(d: dict) -> None:
|
|
24
20
|
"""Validate given value is valid message body."""
|
|
25
21
|
try:
|
|
26
|
-
MessageBody.
|
|
27
|
-
except
|
|
22
|
+
MessageBody.from_any(d)
|
|
23
|
+
except Exception as exc:
|
|
28
24
|
err = _convert_errors(exc)
|
|
29
25
|
raise err from exc
|
|
30
26
|
|
|
31
27
|
|
|
32
|
-
def _convert_errors(exc:
|
|
33
|
-
|
|
34
|
-
errors = [
|
|
35
|
-
ValidationError(
|
|
36
|
-
_("Input validation failed [msg=%(msg)r, input=%(input)r]"),
|
|
37
|
-
code=", ".join(map(str, err["loc"])),
|
|
38
|
-
params={
|
|
39
|
-
"msg": err["msg"],
|
|
40
|
-
"input": err["input"],
|
|
41
|
-
},
|
|
42
|
-
)
|
|
43
|
-
for err in exc.errors()
|
|
44
|
-
]
|
|
45
|
-
return ValidationError(errors)
|
|
28
|
+
def _convert_errors(exc: Exception) -> ValidationError:
|
|
29
|
+
return ValidationError(str(exc))
|
|
@@ -2,43 +2,92 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Any, List, Optional
|
|
8
10
|
|
|
11
|
+
NoneType = type(None)
|
|
9
12
|
|
|
10
|
-
class MessageHeader(BaseModel):
|
|
11
|
-
"""Type definition for message header."""
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
@dataclass
|
|
15
|
+
class MessageHeader:
|
|
16
|
+
"""Message header data definition."""
|
|
15
17
|
|
|
16
|
-
mrkdwn: Optional[str] = None # noqa: UP007
|
|
17
|
-
parse: Optional[str] = None # noqa: UP007
|
|
18
|
-
reply_broadcast: Optional[bool] = None # noqa: UP007
|
|
19
|
-
thread_ts: Optional[str] = None # noqa: UP007
|
|
20
|
-
unfurl_links: Optional[bool] = None # noqa: UP007
|
|
21
|
-
unfurl_media: Optional[bool] = None # noqa: UP007
|
|
18
|
+
mrkdwn: Optional[str] = field(default=None) # noqa: UP007
|
|
19
|
+
parse: Optional[str] = field(default=None) # noqa: UP007
|
|
20
|
+
reply_broadcast: Optional[bool] = field(default=None) # noqa: UP007
|
|
21
|
+
thread_ts: Optional[str] = field(default=None) # noqa: UP007
|
|
22
|
+
unfurl_links: Optional[bool] = field(default=None) # noqa: UP007
|
|
23
|
+
unfurl_media: Optional[bool] = field(default=None) # noqa: UP007
|
|
22
24
|
|
|
25
|
+
def __post_init__(self) -> None:
|
|
26
|
+
_assert_type(self.mrkdwn, (str, NoneType))
|
|
27
|
+
_assert_type(self.parse, (str, NoneType))
|
|
28
|
+
_assert_type(self.reply_broadcast, (bool, NoneType))
|
|
29
|
+
_assert_type(self.thread_ts, (str, NoneType))
|
|
30
|
+
_assert_type(self.unfurl_links, (bool, NoneType))
|
|
31
|
+
_assert_type(self.unfurl_media, (bool, NoneType))
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_any(
|
|
35
|
+
cls,
|
|
36
|
+
obj: MessageHeader | dict[str, Any] | None = None,
|
|
37
|
+
) -> MessageHeader:
|
|
38
|
+
"""Create instance from compatible types."""
|
|
39
|
+
if obj is None:
|
|
40
|
+
return cls()
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
if isinstance(obj, dict):
|
|
43
|
+
return cls(**obj)
|
|
44
|
+
|
|
45
|
+
msg = f"Unsupported type {type(obj)}"
|
|
46
|
+
raise TypeError(msg)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class MessageBody:
|
|
51
|
+
"""Data definition for message body."""
|
|
52
|
+
|
|
53
|
+
attachments: Optional[List[dict]] = field(default=None) # noqa: UP006, UP007
|
|
28
54
|
|
|
29
55
|
# See more about blocks at https://api.slack.com/reference/block-kit/blocks
|
|
30
|
-
blocks: Optional[List[dict]] = None # noqa: UP006, UP007
|
|
56
|
+
blocks: Optional[List[dict]] = field(default=None) # noqa: UP006, UP007
|
|
57
|
+
|
|
58
|
+
text: Optional[str] = field(default=None) # noqa: UP007
|
|
59
|
+
icon_emoji: Optional[str] = field(default=None) # noqa: UP007
|
|
60
|
+
icon_url: Optional[str] = field(default=None) # noqa: UP007
|
|
61
|
+
metadata: Optional[dict] = field(default=None) # noqa: UP007
|
|
62
|
+
username: Optional[str] = field(default=None) # noqa: UP007
|
|
31
63
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
def __post_init__(self) -> None:
|
|
65
|
+
_assert_type(self.attachments, (list, NoneType))
|
|
66
|
+
_assert_type(self.blocks, (list, NoneType))
|
|
67
|
+
_assert_type(self.text, (str, NoneType))
|
|
68
|
+
_assert_type(self.icon_emoji, (str, NoneType))
|
|
69
|
+
_assert_type(self.icon_url, (str, NoneType))
|
|
70
|
+
_assert_type(self.metadata, (dict, NoneType))
|
|
71
|
+
_assert_type(self.username, (str, NoneType))
|
|
37
72
|
|
|
38
|
-
|
|
39
|
-
def _check_one_of_exists(self) -> MessageBody:
|
|
40
|
-
if not self.attachments and not self.blocks and not self.text:
|
|
73
|
+
if not any((self.attachments, self.blocks, self.text)):
|
|
41
74
|
msg = "At least one of `attachments`, `blocks` and `text` must set"
|
|
42
75
|
raise ValueError(msg)
|
|
43
76
|
|
|
44
|
-
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_any(cls, obj: str | MessageBody | dict[str, Any]) -> MessageBody:
|
|
79
|
+
"""Create instance from compatible types."""
|
|
80
|
+
if isinstance(obj, dict):
|
|
81
|
+
return cls(**obj)
|
|
82
|
+
|
|
83
|
+
if isinstance(obj, str):
|
|
84
|
+
return cls(text=obj)
|
|
85
|
+
|
|
86
|
+
msg = f"Unsupported type {type(obj)}"
|
|
87
|
+
raise TypeError(msg)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _assert_type(obj: Any, cls: type | tuple[type, ...]) -> None:
|
|
91
|
+
if not isinstance(obj, cls):
|
|
92
|
+
msg = f"Invalid value type, expected {cls}, got {type(obj)}"
|
|
93
|
+
raise TypeError(msg)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Abstraction for dictionary templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseTemplate(ABC):
|
|
13
|
+
"""Abstract base class for dictionary templates."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def render(self, *, context: dict[str, Any] | None = None) -> dict:
|
|
17
|
+
"""Render template with given context."""
|