agentcore-notifier 0.1.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.
Files changed (57) hide show
  1. agentcore_notifier/__init__.py +3 -0
  2. agentcore_notifier/adapters/__init__.py +1 -0
  3. agentcore_notifier/adapters/django/__init__.py +1 -0
  4. agentcore_notifier/adapters/django/admin.py +47 -0
  5. agentcore_notifier/adapters/django/apps.py +14 -0
  6. agentcore_notifier/adapters/django/cleanup.py +80 -0
  7. agentcore_notifier/adapters/django/conf.py +164 -0
  8. agentcore_notifier/adapters/django/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  9. agentcore_notifier/adapters/django/locale/zh_Hans/LC_MESSAGES/django.po +74 -0
  10. agentcore_notifier/adapters/django/migrations/0001_initial.py +206 -0
  11. agentcore_notifier/adapters/django/migrations/0002_webhookchannel.py +53 -0
  12. agentcore_notifier/adapters/django/migrations/0003_notificationrecord_channel_id.py +26 -0
  13. agentcore_notifier/adapters/django/migrations/0004_notificationchannel_notificationrecord_uuid.py +72 -0
  14. agentcore_notifier/adapters/django/migrations/0005_notificationchannel_user.py +27 -0
  15. agentcore_notifier/adapters/django/migrations/0006_rename_notifier_no_provider_source_4a7c80_idx_notifier_no_provide_fcf309_idx_and_more.py +54 -0
  16. agentcore_notifier/adapters/django/migrations/0007_require_channel_name_and_time_windows.py +32 -0
  17. agentcore_notifier/adapters/django/migrations/0008_alter_notificationchannel_channel_type.py +18 -0
  18. agentcore_notifier/adapters/django/migrations/0009_alter_notificationchannel_channel_type.py +18 -0
  19. agentcore_notifier/adapters/django/migrations/__init__.py +0 -0
  20. agentcore_notifier/adapters/django/models.py +227 -0
  21. agentcore_notifier/adapters/django/periodic_tasks.py +43 -0
  22. agentcore_notifier/adapters/django/serializers.py +46 -0
  23. agentcore_notifier/adapters/django/services/__init__.py +9 -0
  24. agentcore_notifier/adapters/django/services/email_service.py +261 -0
  25. agentcore_notifier/adapters/django/services/feishu_app/__init__.py +8 -0
  26. agentcore_notifier/adapters/django/services/feishu_app/client.py +132 -0
  27. agentcore_notifier/adapters/django/services/feishu_app/crypto.py +123 -0
  28. agentcore_notifier/adapters/django/services/feishu_app/device_registration.py +168 -0
  29. agentcore_notifier/adapters/django/services/feishu_app/oauth.py +89 -0
  30. agentcore_notifier/adapters/django/services/feishu_app/token.py +132 -0
  31. agentcore_notifier/adapters/django/services/merge_and_silence.py +264 -0
  32. agentcore_notifier/adapters/django/services/notification_config.py +39 -0
  33. agentcore_notifier/adapters/django/services/notification_stats.py +385 -0
  34. agentcore_notifier/adapters/django/services/notification_test.py +199 -0
  35. agentcore_notifier/adapters/django/services/webhook/__init__.py +25 -0
  36. agentcore_notifier/adapters/django/services/webhook/base.py +16 -0
  37. agentcore_notifier/adapters/django/services/webhook/feishu.py +128 -0
  38. agentcore_notifier/adapters/django/services/webhook/registry.py +80 -0
  39. agentcore_notifier/adapters/django/services/webhook/wechat.py +56 -0
  40. agentcore_notifier/adapters/django/services/webhook_service.py +440 -0
  41. agentcore_notifier/adapters/django/services/wecom/__init__.py +17 -0
  42. agentcore_notifier/adapters/django/services/wecom/client.py +153 -0
  43. agentcore_notifier/adapters/django/services/wecom/device_registration.py +170 -0
  44. agentcore_notifier/adapters/django/tasks/__init__.py +14 -0
  45. agentcore_notifier/adapters/django/tasks/cleanup.py +117 -0
  46. agentcore_notifier/adapters/django/tasks/send.py +485 -0
  47. agentcore_notifier/adapters/django/urls.py +80 -0
  48. agentcore_notifier/adapters/django/views/__init__.py +34 -0
  49. agentcore_notifier/adapters/django/views/channels.py +705 -0
  50. agentcore_notifier/adapters/django/views/config.py +54 -0
  51. agentcore_notifier/adapters/django/views/stats.py +126 -0
  52. agentcore_notifier/constants.py +65 -0
  53. agentcore_notifier-0.1.0.dist-info/METADATA +196 -0
  54. agentcore_notifier-0.1.0.dist-info/RECORD +57 -0
  55. agentcore_notifier-0.1.0.dist-info/WHEEL +5 -0
  56. agentcore_notifier-0.1.0.dist-info/licenses/LICENSE +202 -0
  57. agentcore_notifier-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """Agentcore Notifier: webhook, email, and related notification management."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """Adapters for agentcore_notifier."""
@@ -0,0 +1 @@
1
+ """Django adapter for agentcore_notifier."""
@@ -0,0 +1,47 @@
1
+ """Admin for agentcore_notifier Django adapter."""
2
+ from django.contrib import admin
3
+ from django.utils.translation import gettext_lazy as _
4
+
5
+ from agentcore_notifier.adapters.django.models import (
6
+ NotificationChannel,
7
+ NotificationRecord,
8
+ NotifierConfig,
9
+ )
10
+
11
+
12
+ @admin.register(NotificationRecord)
13
+ class NotificationRecordAdmin(admin.ModelAdmin):
14
+ list_display = [
15
+ "id",
16
+ "provider_type",
17
+ "source_app",
18
+ "source_type",
19
+ "status",
20
+ "created_at",
21
+ "sent_at",
22
+ ]
23
+ list_filter = ["provider_type", "status", "source_app", "created_at"]
24
+ search_fields = ["source_app", "source_type", "source_id"]
25
+ readonly_fields = ["created_at", "sent_at"]
26
+ date_hierarchy = "created_at"
27
+ ordering = ["-created_at"]
28
+
29
+
30
+ @admin.register(NotifierConfig)
31
+ class NotifierConfigAdmin(admin.ModelAdmin):
32
+ list_display = ["scope", "user", "key", "updated_at"]
33
+ list_filter = ["scope", "key"]
34
+ search_fields = ["key"]
35
+ raw_id_fields = ["user"]
36
+ ordering = ["scope", "key"]
37
+
38
+
39
+ @admin.register(NotificationChannel)
40
+ class NotificationChannelAdmin(admin.ModelAdmin):
41
+ list_display = [
42
+ "id", "channel_type", "name", "is_active", "is_default",
43
+ "ordering", "created_at",
44
+ ]
45
+ list_filter = ["channel_type", "is_active", "is_default"]
46
+ search_fields = ["name"]
47
+ ordering = ["ordering", "created_at"]
@@ -0,0 +1,14 @@
1
+ """Django app config for agentcore_notifier."""
2
+ from django.apps import AppConfig
3
+
4
+
5
+ class AgentcoreNotifierDjangoConfig(AppConfig):
6
+ """App config for agentcore_notifier Django adapter."""
7
+
8
+ default_auto_field = "django.db.models.BigAutoField"
9
+ name = "agentcore_notifier.adapters.django"
10
+ label = "agentcore_notifier"
11
+ verbose_name = "Agentcore Notifier"
12
+
13
+ def ready(self):
14
+ pass
@@ -0,0 +1,80 @@
1
+ """
2
+ Cleanup of old notification records.
3
+ Uses conf when arguments are omitted. Call directly or via Celery task.
4
+ """
5
+ import logging
6
+ from datetime import timedelta
7
+ from typing import Any, Dict, Optional
8
+
9
+ from django.utils import timezone
10
+
11
+ from agentcore_notifier.adapters.django.conf import (
12
+ get_cleanup_only_completed,
13
+ get_retention_days,
14
+ )
15
+ from agentcore_notifier.adapters.django.models import NotificationRecord
16
+ from agentcore_notifier.constants import Status
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ DEFAULT_BATCH_SIZE = 5000
21
+
22
+
23
+ def cleanup_old_notification_records(
24
+ retention_days: Optional[int] = None,
25
+ only_completed: Optional[bool] = None,
26
+ batch_size: Optional[int] = None,
27
+ ) -> Dict[str, Any]:
28
+ """
29
+ Delete notification records older than retention_days.
30
+ only_completed=True: only delete status in (success, failed).
31
+ """
32
+ if retention_days is None:
33
+ retention_days = get_retention_days()
34
+ if only_completed is None:
35
+ only_completed = get_cleanup_only_completed()
36
+
37
+ if retention_days <= 0:
38
+ logger.warning(
39
+ f"cleanup_old_notification_records: retention_days="
40
+ f"{retention_days} <= 0, skipping"
41
+ )
42
+ return {
43
+ "deleted_count": 0,
44
+ "cutoff": timezone.now(),
45
+ "retention_days": retention_days,
46
+ "only_completed": only_completed,
47
+ "skipped": True,
48
+ "reason": "invalid_retention_days",
49
+ }
50
+
51
+ cutoff = timezone.now() - timedelta(days=retention_days)
52
+ base_qs = NotificationRecord.objects.filter(created_at__lt=cutoff)
53
+ if only_completed:
54
+ base_qs = base_qs.filter(status__in=(Status.SUCCESS, Status.FAILED))
55
+
56
+ # Delete in one go or in batches
57
+ if batch_size is None or batch_size <= 0:
58
+ deleted_count, _ = base_qs.delete()
59
+ total_deleted = deleted_count
60
+ else:
61
+ total_deleted = 0
62
+ while True:
63
+ batch = list(base_qs.values_list("pk", flat=True)[:batch_size])
64
+ if not batch:
65
+ break
66
+ batch_deleted, _ = (
67
+ NotificationRecord.objects.filter(pk__in=batch).delete()
68
+ )
69
+ total_deleted += batch_deleted
70
+
71
+ logger.info(
72
+ f"cleanup_old_notification_records: deleted={total_deleted} "
73
+ f"retention_days={retention_days} only_completed={only_completed}"
74
+ )
75
+ return {
76
+ "deleted_count": total_deleted,
77
+ "cutoff": cutoff,
78
+ "retention_days": retention_days,
79
+ "only_completed": only_completed,
80
+ }
@@ -0,0 +1,164 @@
1
+ """
2
+ Global config for agentcore_notifier (cleanup, merge). Not user-specific.
3
+ Uses lazy imports of notification_config to avoid circular import.
4
+ """
5
+ try:
6
+ from celery.schedules import crontab
7
+ except ImportError:
8
+ crontab = None
9
+
10
+ from django.conf import settings
11
+
12
+ DEFAULT_RETENTION_DAYS = 180
13
+ DEFAULT_CLEANUP_ONLY_COMPLETED = True
14
+ DEFAULT_CLEANUP_ENABLED = True
15
+ DEFAULT_CLEANUP_CRONTAB = "0 2 * * *"
16
+
17
+
18
+ def _get_global_config():
19
+ # NOTE(Ray): Lazy import to avoid circular import.
20
+ from agentcore_notifier.adapters.django.services import notification_config
21
+
22
+ raw = notification_config.get_config("global")
23
+ return raw if isinstance(raw, dict) else {}
24
+
25
+
26
+ def get_retention_days():
27
+ """
28
+ Retention days for cleanup: NotifierConfig key=global first, else settings.
29
+ """
30
+ g = _get_global_config()
31
+ v = g.get("retention_days")
32
+ if isinstance(v, int) and v > 0:
33
+ return v
34
+ return getattr(
35
+ settings,
36
+ "AGENTCORE_NOTIFIER_RETENTION_DAYS",
37
+ DEFAULT_RETENTION_DAYS,
38
+ )
39
+
40
+
41
+ def get_cleanup_only_completed():
42
+ """Return whether cleanup deletes only completed records (default True)."""
43
+ return getattr(
44
+ settings,
45
+ "AGENTCORE_NOTIFIER_CLEANUP_ONLY_COMPLETED",
46
+ DEFAULT_CLEANUP_ONLY_COMPLETED,
47
+ )
48
+
49
+
50
+ def get_cleanup_enabled():
51
+ """Return whether cleanup beat task is enabled (default True)."""
52
+ g = _get_global_config()
53
+ if "cleanup_enabled" in g:
54
+ return bool(g["cleanup_enabled"])
55
+ return getattr(
56
+ settings, "AGENTCORE_NOTIFIER_CLEANUP_ENABLED", DEFAULT_CLEANUP_ENABLED
57
+ )
58
+
59
+
60
+ def get_cleanup_crontab():
61
+ """Return 5-field cron expression for cleanup (default daily 2:00)."""
62
+ g = _get_global_config()
63
+ v = g.get("cleanup_crontab")
64
+ if isinstance(v, str) and v.strip():
65
+ return v.strip()
66
+ return getattr(
67
+ settings, "AGENTCORE_NOTIFIER_CLEANUP_CRONTAB", DEFAULT_CLEANUP_CRONTAB
68
+ )
69
+
70
+
71
+ def _crontab_from_expression(expr):
72
+ """Parse 5-field cron into Celery crontab. On parse error returns None."""
73
+ if not crontab or not expr:
74
+ return None
75
+ parts = str(expr).strip().split()
76
+ if len(parts) != 5:
77
+ return None
78
+ try:
79
+ return crontab(
80
+ minute=parts[0],
81
+ hour=parts[1],
82
+ day_of_month=parts[2],
83
+ month_of_year=parts[3],
84
+ day_of_week=parts[4],
85
+ )
86
+ except (TypeError, ValueError):
87
+ return None
88
+
89
+
90
+ def get_cleanup_beat_schedule(interval_hours=None):
91
+ """Beat schedule for cleanup. Uses get_cleanup_crontab() or interval."""
92
+ task_name = (
93
+ "agentcore_notifier.adapters.django.tasks.cleanup."
94
+ "cleanup_old_notification_records_task"
95
+ )
96
+ if interval_hours is not None:
97
+ schedule = interval_hours * 3600.0
98
+ else:
99
+ schedule = _crontab_from_expression(get_cleanup_crontab())
100
+ if schedule is None:
101
+ schedule = 24 * 3600.0
102
+ return {
103
+ "agentcore-notifier-cleanup-old-records": {
104
+ "task": task_name,
105
+ "schedule": schedule,
106
+ "options": {},
107
+ }
108
+ }
109
+
110
+
111
+ def get_cleanup_beat_schedule_init(interval_hours=None):
112
+ """
113
+ Build cleanup beat schedule from Django settings only (no DB).
114
+ For use in AppConfig.ready() to avoid database-during-init warning.
115
+ Runtime DB config is still applied when the cleanup task runs.
116
+ """
117
+ task_name = (
118
+ "agentcore_notifier.adapters.django.tasks.cleanup."
119
+ "cleanup_old_notification_records_task"
120
+ )
121
+ if interval_hours is not None:
122
+ schedule = interval_hours * 3600.0
123
+ else:
124
+ crontab_str = getattr(
125
+ settings,
126
+ "AGENTCORE_NOTIFIER_CLEANUP_CRONTAB",
127
+ DEFAULT_CLEANUP_CRONTAB,
128
+ )
129
+ schedule = _crontab_from_expression(crontab_str)
130
+ if schedule is None:
131
+ schedule = 24 * 3600.0
132
+ return {
133
+ "agentcore-notifier-cleanup-old-records": {
134
+ "task": task_name,
135
+ "schedule": schedule,
136
+ "options": {},
137
+ }
138
+ }
139
+
140
+
141
+ def get_merge_enabled(provider_type: str) -> bool:
142
+ """Return whether merge is enabled for this provider (default False)."""
143
+ # NOTE(Ray): Lazy import to avoid circular import.
144
+ from agentcore_notifier.adapters.django.services import notification_config
145
+
146
+ key = f"channel_{provider_type}"
147
+ raw = notification_config.get_config(key)
148
+ if isinstance(raw, dict) and "merge_enabled" in raw:
149
+ return bool(raw["merge_enabled"])
150
+ return False
151
+
152
+
153
+ def get_merge_window_minutes(provider_type: str):
154
+ """Return merge window in minutes for this provider, or None (disabled)."""
155
+ # NOTE(Ray): Lazy import to avoid circular import.
156
+ from agentcore_notifier.adapters.django.services import notification_config
157
+
158
+ key = f"channel_{provider_type}"
159
+ raw = notification_config.get_config(key)
160
+ if isinstance(raw, dict):
161
+ v = raw.get("merge_window_minutes")
162
+ if isinstance(v, int) and v > 0:
163
+ return v
164
+ return None
@@ -0,0 +1,74 @@
1
+ # agentcore-notifier Chinese (Simplified) translations
2
+ msgid ""
3
+ msgstr ""
4
+ "Project-Id-Version: agentcore-notifier\n"
5
+ "Report-Msgid-Bugs-To: \n"
6
+ "POT-Creation-Date: 2026-06-25 00:00+0800\n"
7
+ "PO-Revision-Date: 2026-06-25 00:00+0800\n"
8
+ "Language-Team: zh_Hans\n"
9
+ "Language: zh_Hans\n"
10
+ "MIME-Version: 1.0\n"
11
+ "Content-Type: text/plain; charset=UTF-8\n"
12
+ "Content-Transfer-Encoding: 8bit\n"
13
+ "Plural-Forms: nplurals=1; plural=0;\n"
14
+
15
+ # views/channels.py — webhook validation
16
+ msgid "Webhook URL not configured"
17
+ msgstr "Webhook 地址未配置,请先填写 Webhook URL"
18
+
19
+ msgid "[%s] Channel validation test"
20
+ msgstr "[%s] 渠道连通性测试"
21
+
22
+ msgid "Send failed"
23
+ msgstr "发送失败,请检查 Webhook 地址及网络连通性"
24
+
25
+ msgid "Validation failed"
26
+ msgstr "验证失败"
27
+
28
+ # views/channels.py — email validation
29
+ msgid "SMTP host not configured"
30
+ msgstr "SMTP 服务器地址未配置"
31
+
32
+ msgid "From address required to send test"
33
+ msgstr "发送测试邮件需要先配置发件人地址(From)"
34
+
35
+ msgid "(connection only)"
36
+ msgstr "(仅测试连接,未发送邮件)"
37
+
38
+ msgid "[%s] Email validation test"
39
+ msgstr "[%s] 邮件渠道连通性测试"
40
+
41
+ msgid "[%s] Email channel validation test"
42
+ msgstr "这是来自 [%s] 的邮件渠道连通性测试,收到此邮件说明 SMTP 配置正确。"
43
+
44
+ msgid ""
45
+ "Server cannot reach SMTP host (no route). "
46
+ "Check deployment: firewall/security group outbound, "
47
+ "or container network. Original: "
48
+ msgstr ""
49
+ "服务器无法访问 SMTP 主机(网络不可达)。"
50
+ "请检查:防火墙/安全组出站规则、容器网络配置是否正确。原始错误:"
51
+
52
+ msgid ""
53
+ "SMTP connection timed out. Check server network and "
54
+ "firewall/proxy. Original: "
55
+ msgstr ""
56
+ "SMTP 连接超时,请检查服务器网络、防火墙或代理配置。原始错误:"
57
+
58
+ # services/webhook_service.py
59
+ msgid "Invalid response format from webhook. Expected JSON but got: {}"
60
+ msgstr "Webhook 返回格式异常,期望 JSON 格式,实际收到:{}"
61
+
62
+ msgid "Webhook request failed with HTTP status {}. Response: {}"
63
+ msgstr "Webhook 请求失败,HTTP 状态码 {},响应内容:{}"
64
+
65
+ msgid ""
66
+ "Webhook validation failed: {}. Please check if the webhook URL is "
67
+ "correct, or verify security settings (e.g., IP whitelist, API key)."
68
+ msgstr ""
69
+ "Webhook 验证失败:{}。请确认 Webhook 地址是否正确,"
70
+ "并检查安全配置(如 IP 白名单、访问密钥)。"
71
+
72
+ # services/webhook/feishu.py & wechat.py
73
+ msgid "Webhook error code {}"
74
+ msgstr "Webhook 业务错误,错误码 {}"
@@ -0,0 +1,206 @@
1
+ # Initial migration for agentcore_notifier Django adapter.
2
+
3
+ from django.conf import settings
4
+ from django.db import migrations, models
5
+ import django.db.models.deletion
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.CreateModel(
18
+ name="NotificationRecord",
19
+ fields=[
20
+ (
21
+ "id",
22
+ models.BigAutoField(
23
+ auto_created=True,
24
+ primary_key=True,
25
+ serialize=False,
26
+ verbose_name="ID",
27
+ ),
28
+ ),
29
+ ("source_app", models.CharField(max_length=100)),
30
+ ("source_type", models.CharField(blank=True, max_length=100)),
31
+ ("source_id", models.CharField(blank=True, max_length=100)),
32
+ (
33
+ "source_metadata",
34
+ models.JSONField(blank=True, default=dict),
35
+ ),
36
+ (
37
+ "channel",
38
+ models.CharField(default="webhook", max_length=50),
39
+ ),
40
+ (
41
+ "provider_type",
42
+ models.CharField(
43
+ choices=[
44
+ ("feishu", "Feishu"),
45
+ ("wecom", "WeCom"),
46
+ ("wechat", "WeChat Work"),
47
+ ("email", "Email"),
48
+ ],
49
+ db_index=True,
50
+ max_length=20,
51
+ ),
52
+ ),
53
+ (
54
+ "target",
55
+ models.JSONField(blank=True, default=dict),
56
+ ),
57
+ ("payload", models.JSONField()),
58
+ (
59
+ "template_key",
60
+ models.CharField(blank=True, max_length=100),
61
+ ),
62
+ (
63
+ "locale",
64
+ models.CharField(blank=True, max_length=20),
65
+ ),
66
+ (
67
+ "content_metadata",
68
+ models.JSONField(blank=True, default=dict),
69
+ ),
70
+ (
71
+ "status",
72
+ models.CharField(
73
+ choices=[
74
+ ("pending", "Pending"),
75
+ ("success", "Success"),
76
+ ("failed", "Failed"),
77
+ ("merged", "Merged"),
78
+ ("silenced", "Silenced"),
79
+ ],
80
+ db_index=True,
81
+ default="pending",
82
+ max_length=20,
83
+ ),
84
+ ),
85
+ ("response", models.JSONField(blank=True, null=True)),
86
+ ("error_message", models.TextField(blank=True)),
87
+ ("sent_at", models.DateTimeField(blank=True, null=True)),
88
+ (
89
+ "provider_message_id",
90
+ models.CharField(blank=True, max_length=255),
91
+ ),
92
+ (
93
+ "metadata",
94
+ models.JSONField(blank=True, default=dict),
95
+ ),
96
+ ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
97
+ (
98
+ "user",
99
+ models.ForeignKey(
100
+ blank=True,
101
+ null=True,
102
+ on_delete=django.db.models.deletion.SET_NULL,
103
+ related_name="+",
104
+ to=settings.AUTH_USER_MODEL,
105
+ ),
106
+ ),
107
+ ],
108
+ options={
109
+ "db_table": "notifier_notification_record",
110
+ "ordering": ["-created_at"],
111
+ "verbose_name": "Notification Record",
112
+ "verbose_name_plural": "Notification Records",
113
+ },
114
+ ),
115
+ migrations.CreateModel(
116
+ name="NotifierConfig",
117
+ fields=[
118
+ (
119
+ "id",
120
+ models.BigAutoField(
121
+ auto_created=True,
122
+ primary_key=True,
123
+ serialize=False,
124
+ verbose_name="ID",
125
+ ),
126
+ ),
127
+ (
128
+ "scope",
129
+ models.CharField(
130
+ choices=[("global", "Global"), ("user", "User")],
131
+ db_index=True,
132
+ default="global",
133
+ max_length=20,
134
+ ),
135
+ ),
136
+ (
137
+ "key",
138
+ models.CharField(db_index=True, max_length=128),
139
+ ),
140
+ (
141
+ "value",
142
+ models.JSONField(
143
+ default=dict,
144
+ help_text="Config payload (JSON).",
145
+ ),
146
+ ),
147
+ ("updated_at", models.DateTimeField(auto_now=True)),
148
+ (
149
+ "user",
150
+ models.ForeignKey(
151
+ blank=True,
152
+ help_text="Null for global scope; set for user-level override.",
153
+ null=True,
154
+ on_delete=django.db.models.deletion.CASCADE,
155
+ related_name="+",
156
+ to=settings.AUTH_USER_MODEL,
157
+ ),
158
+ ),
159
+ ],
160
+ options={
161
+ "db_table": "agentcore_notifier_config",
162
+ "verbose_name": "Notifier Config",
163
+ "verbose_name_plural": "Notifier Configs",
164
+ },
165
+ ),
166
+ migrations.AddIndex(
167
+ model_name="notificationrecord",
168
+ index=models.Index(
169
+ fields=[
170
+ "provider_type",
171
+ "source_app",
172
+ "source_type",
173
+ "source_id",
174
+ ],
175
+ name="notifier_no_provider_source_4a7c80_idx",
176
+ ),
177
+ ),
178
+ migrations.AddIndex(
179
+ model_name="notificationrecord",
180
+ index=models.Index(
181
+ fields=["status", "created_at"],
182
+ name="notifier_no_status_created_2b0f3a_idx",
183
+ ),
184
+ ),
185
+ migrations.AddIndex(
186
+ model_name="notificationrecord",
187
+ index=models.Index(
188
+ fields=["created_at"],
189
+ name="notifier_no_created_at_1f8d9e_idx",
190
+ ),
191
+ ),
192
+ migrations.AddConstraint(
193
+ model_name="notifierconfig",
194
+ constraint=models.UniqueConstraint(
195
+ fields=("scope", "user", "key"),
196
+ name="agentcore_notifier_config_scope_user_key_uniq",
197
+ ),
198
+ ),
199
+ migrations.AddIndex(
200
+ model_name="notifierconfig",
201
+ index=models.Index(
202
+ fields=["scope", "key"],
203
+ name="agentcore_notifier_config_scope_key_idx",
204
+ ),
205
+ ),
206
+ ]
@@ -0,0 +1,53 @@
1
+ # NotificationChannel model: single table for webhook/email/sms channels.
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("agentcore_notifier", "0001_initial"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.CreateModel(
14
+ name="NotificationChannel",
15
+ fields=[
16
+ ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
17
+ (
18
+ "channel_type",
19
+ models.CharField(
20
+ choices=[("webhook", "Webhook"), ("email", "Email"), ("sms", "SMS")],
21
+ db_index=True,
22
+ max_length=20,
23
+ ),
24
+ ),
25
+ ("name", models.CharField(blank=True, max_length=255)),
26
+ ("is_active", models.BooleanField(default=True)),
27
+ (
28
+ "is_default",
29
+ models.BooleanField(
30
+ default=False,
31
+ help_text="If True, this channel is used for sending when no channel is specified.",
32
+ ),
33
+ ),
34
+ ("ordering", models.PositiveIntegerField(default=0)),
35
+ (
36
+ "config",
37
+ models.JSONField(
38
+ blank=True,
39
+ default=dict,
40
+ help_text="Type-specific config, e.g. webhook: provider_type, url; email: smtp_host, port; sms: provider, api_key.",
41
+ ),
42
+ ),
43
+ ("created_at", models.DateTimeField(auto_now_add=True)),
44
+ ("updated_at", models.DateTimeField(auto_now=True)),
45
+ ],
46
+ options={
47
+ "db_table": "notifier_channel",
48
+ "ordering": ["ordering", "created_at"],
49
+ "verbose_name": "Notification Channel",
50
+ "verbose_name_plural": "Notification Channels",
51
+ },
52
+ ),
53
+ ]
@@ -0,0 +1,26 @@
1
+ # Add channel_id to NotificationRecord for per-channel merge scope.
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("agentcore_notifier", "0002_webhookchannel"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="notificationrecord",
16
+ name="channel_link",
17
+ field=models.ForeignKey(
18
+ blank=True,
19
+ db_column="channel_id",
20
+ null=True,
21
+ on_delete=django.db.models.deletion.SET_NULL,
22
+ related_name="notification_records",
23
+ to="NotificationChannel",
24
+ ),
25
+ ),
26
+ ]