notifyfork 0.1.2__tar.gz → 0.1.3__tar.gz

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 (99) hide show
  1. {notifyfork-0.1.2 → notifyfork-0.1.3}/PKG-INFO +1 -1
  2. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/application/use_cases/send_notification.py +29 -8
  3. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/domain/entities/notification.py +4 -4
  4. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/domain/events/notification_events.py +4 -4
  5. notifyfork-0.1.3/notifyfork/core/infrastructure/admin.py +36 -0
  6. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/providers/twilio_provider.py +1 -1
  7. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/providers/whatsapp_provider.py +6 -2
  8. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/repositories/notification_repository.py +2 -0
  9. {notifyfork-0.1.2 → notifyfork-0.1.3}/pyproject.toml +1 -1
  10. {notifyfork-0.1.2 → notifyfork-0.1.3}/.env.example +0 -0
  11. {notifyfork-0.1.2 → notifyfork-0.1.3}/.github/workflows/auto-tag.yml +0 -0
  12. {notifyfork-0.1.2 → notifyfork-0.1.3}/.github/workflows/ci.yml +0 -0
  13. {notifyfork-0.1.2 → notifyfork-0.1.3}/.github/workflows/publish.yml +0 -0
  14. {notifyfork-0.1.2 → notifyfork-0.1.3}/.gitignore +0 -0
  15. {notifyfork-0.1.2 → notifyfork-0.1.3}/CHANGELOG.md +0 -0
  16. {notifyfork-0.1.2 → notifyfork-0.1.3}/CONTRIBUTING.md +0 -0
  17. {notifyfork-0.1.2 → notifyfork-0.1.3}/LICENSE +0 -0
  18. {notifyfork-0.1.2 → notifyfork-0.1.3}/README.md +0 -0
  19. {notifyfork-0.1.2 → notifyfork-0.1.3}/examples/README.md +0 -0
  20. {notifyfork-0.1.2 → notifyfork-0.1.3}/examples/custom_provider/register_xpto_provider.py +0 -0
  21. {notifyfork-0.1.2 → notifyfork-0.1.3}/examples/email/send_order_confirmed.py +0 -0
  22. {notifyfork-0.1.2 → notifyfork-0.1.3}/examples/email/send_sendgrid_external.py +0 -0
  23. {notifyfork-0.1.2 → notifyfork-0.1.3}/examples/push/send_flash_sale.py +0 -0
  24. {notifyfork-0.1.2 → notifyfork-0.1.3}/examples/slack/send_system_error.py +0 -0
  25. {notifyfork-0.1.2 → notifyfork-0.1.3}/examples/sms/send_otp.py +0 -0
  26. {notifyfork-0.1.2 → notifyfork-0.1.3}/examples/whatsapp/send_order_shipped.py +0 -0
  27. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/__init__.py +0 -0
  28. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/api/__init__.py +0 -0
  29. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/api/urls.py +0 -0
  30. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/api/webhooks/__init__.py +0 -0
  31. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/api/webhooks/resend_webhook.py +0 -0
  32. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/api/webhooks/sendgrid_webhook.py +0 -0
  33. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/api/webhooks/tasks.py +0 -0
  34. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/api/webhooks/twilio_webhook.py +0 -0
  35. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/api/webhooks/urls.py +0 -0
  36. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/client.py +0 -0
  37. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/__init__.py +0 -0
  38. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/application/__init__.py +0 -0
  39. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/application/dtos/__init__.py +0 -0
  40. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/application/dtos/send_notification_dto.py +0 -0
  41. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/application/interfaces/__init__.py +0 -0
  42. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/application/interfaces/notification_provider.py +0 -0
  43. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/application/interfaces/notification_repository.py +0 -0
  44. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/application/interfaces/template_repository.py +0 -0
  45. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/application/use_cases/__init__.py +0 -0
  46. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/domain/__init__.py +0 -0
  47. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/domain/entities/__init__.py +0 -0
  48. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/domain/events/__init__.py +0 -0
  49. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/domain/value_objects/__init__.py +0 -0
  50. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/domain/value_objects/template.py +0 -0
  51. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/__init__.py +0 -0
  52. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/apps.py +0 -0
  53. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/container/__init__.py +0 -0
  54. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/container/providers.py +0 -0
  55. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/migrations/0001_initial.py +0 -0
  56. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/migrations/0002_seed_templates.py +0 -0
  57. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/migrations/0003_delivery_status.py +0 -0
  58. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/migrations/__init__.py +0 -0
  59. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/models/__init__.py +0 -0
  60. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/models/notification_model.py +0 -0
  61. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/providers/__init__.py +0 -0
  62. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/providers/firebase_provider.py +0 -0
  63. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/providers/resend_provider.py +0 -0
  64. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/providers/sendgrid_provider.py +0 -0
  65. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/providers/slack_provider.py +0 -0
  66. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/providers/smtp_provider.py +0 -0
  67. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/queue/__init__.py +0 -0
  68. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/queue/tasks.py +0 -0
  69. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/repositories/__init__.py +0 -0
  70. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/core/infrastructure/repositories/template_repository.py +0 -0
  71. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/shared/__init__.py +0 -0
  72. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/shared/exceptions/__init__.py +0 -0
  73. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/shared/exceptions/provider_exceptions.py +0 -0
  74. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/shared/logging/__init__.py +0 -0
  75. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/shared/logging/setup.py +0 -0
  76. {notifyfork-0.1.2 → notifyfork-0.1.3}/notifyfork/shared/utils/__init__.py +0 -0
  77. {notifyfork-0.1.2 → notifyfork-0.1.3}/requirements.txt +0 -0
  78. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/__init__.py +0 -0
  79. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/conftest.py +0 -0
  80. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/integration/__init__.py +0 -0
  81. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/__init__.py +0 -0
  82. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/api/__init__.py +0 -0
  83. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/application/__init__.py +0 -0
  84. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/application/test_send_notification_use_case.py +0 -0
  85. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/conftest.py +0 -0
  86. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/domain/__init__.py +0 -0
  87. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/domain/test_notification_entity.py +0 -0
  88. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/domain/test_template_value_object.py +0 -0
  89. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/providers/__init__.py +0 -0
  90. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/providers/test_resend_provider.py +0 -0
  91. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/providers/test_sendgrid_provider.py +0 -0
  92. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/providers/test_slack_provider.py +0 -0
  93. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/providers/test_whatsapp_provider.py +0 -0
  94. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/test_client.py +0 -0
  95. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/test_container.py +0 -0
  96. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/test_custom_provider.py +0 -0
  97. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/webhooks/__init__.py +0 -0
  98. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/unit/webhooks/test_delivery_status.py +0 -0
  99. {notifyfork-0.1.2 → notifyfork-0.1.3}/tests/urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notifyfork
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Provider-agnostic notification gateway for Django. Send to SMS, Email, WhatsApp, Push, and Slack via a single API.
5
5
  Project-URL: Homepage, https://github.com/marioasaraujo/notifyfork
6
6
  Project-URL: Documentation, https://github.com/marioasaraujo/notifyfork#readme
@@ -57,18 +57,39 @@ class SendNotificationUseCase:
57
57
 
58
58
  error = "Unknown provider error"
59
59
  for index, provider in enumerate(candidates):
60
- # Provider handles LOCAL vs EXTERNAL rendering internally
61
- result = await provider.send_with_template(
62
- recipient=dto.recipient,
63
- template=template,
64
- context=dto.context,
65
- )
60
+ has_fallback = index < len(candidates) - 1
61
+ try:
62
+ # Provider handles LOCAL vs EXTERNAL rendering internally
63
+ result = await provider.send_with_template(
64
+ recipient=dto.recipient,
65
+ template=template,
66
+ context=dto.context,
67
+ )
68
+ except Exception as exc:
69
+ # A provider raising instead of returning ProviderResult(success=False)
70
+ # must not escape this loop: an uncaught exception here propagates to
71
+ # the Celery task, which retries the whole use case from scratch
72
+ # (creating a brand-new Notification each time) and this notification
73
+ # is never saved as FAILED — it's left stuck in QUEUED forever, even
74
+ # after Celery's own retries are exhausted.
75
+ error = str(exc)
76
+ logger.warning(
77
+ "Provider raised an exception"
78
+ + (", falling back to next provider" if has_fallback else ""),
79
+ extra={
80
+ "provider": provider.name,
81
+ "channel": dto.channel,
82
+ "error": error,
83
+ "fallback_available": has_fallback,
84
+ },
85
+ )
86
+ continue
87
+
66
88
  if result.success:
67
- notification.mark_sent(provider.name)
89
+ notification.mark_sent(provider.name, result.external_id)
68
90
  break
69
91
 
70
92
  error = result.error or "Unknown provider error"
71
- has_fallback = index < len(candidates) - 1
72
93
  logger.warning(
73
94
  "Provider failed" + (", falling back to next provider" if has_fallback else ""),
74
95
  extra={
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from datetime import datetime
2
+ from datetime import datetime, timezone
3
3
  from enum import Enum
4
4
  from typing import Any
5
5
  from uuid import UUID, uuid4
@@ -61,7 +61,7 @@ class Notification:
61
61
  attempts: int = 0
62
62
  max_attempts: int = 3
63
63
  error_detail: str | None = None
64
- created_at: datetime = field(default_factory=datetime.utcnow)
64
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
65
65
  sent_at: datetime | None = None
66
66
  delivered_at: datetime | None = None
67
67
 
@@ -75,7 +75,7 @@ class Notification:
75
75
  self.status = NotificationStatus.SENT
76
76
  self.provider_used = provider
77
77
  self.provider_message_id = provider_message_id
78
- self.sent_at = datetime.utcnow()
78
+ self.sent_at = datetime.now(timezone.utc)
79
79
  logger.info("Notification sent — awaiting delivery confirmation", extra={
80
80
  "notification_id": str(self.id),
81
81
  "provider": provider,
@@ -90,7 +90,7 @@ class Notification:
90
90
  "notification_id": str(self.id), "current_status": self.status,
91
91
  })
92
92
  self.status = NotificationStatus.DELIVERED
93
- self.delivered_at = datetime.utcnow()
93
+ self.delivered_at = datetime.now(timezone.utc)
94
94
  logger.info("Notification delivered", extra={
95
95
  "notification_id": str(self.id),
96
96
  "provider": self.provider_used,
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from datetime import datetime
2
+ from datetime import datetime, timezone
3
3
  from uuid import UUID
4
4
 
5
5
 
@@ -8,14 +8,14 @@ class NotificationQueued:
8
8
  notification_id: UUID
9
9
  channel: str
10
10
  recipient: str
11
- occurred_at: datetime = field(default_factory=datetime.utcnow)
11
+ occurred_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
12
12
 
13
13
 
14
14
  @dataclass(frozen=True)
15
15
  class NotificationSent:
16
16
  notification_id: UUID
17
17
  provider: str
18
- occurred_at: datetime = field(default_factory=datetime.utcnow)
18
+ occurred_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
19
19
 
20
20
 
21
21
  @dataclass(frozen=True)
@@ -23,4 +23,4 @@ class NotificationFailed:
23
23
  notification_id: UUID
24
24
  reason: str
25
25
  attempts: int
26
- occurred_at: datetime = field(default_factory=datetime.utcnow)
26
+ occurred_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@@ -0,0 +1,36 @@
1
+ from django.contrib import admin
2
+
3
+ from notifyfork.core.infrastructure.models import NotificationTemplateModel
4
+
5
+
6
+ @admin.register(NotificationTemplateModel)
7
+ class NotificationTemplateAdmin(admin.ModelAdmin):
8
+ list_display = ("id", "mode", "body_kind", "is_active", "updated_at")
9
+ list_filter = ("mode", "is_active")
10
+ search_fields = ("id",)
11
+ readonly_fields = ("created_at", "updated_at")
12
+
13
+ fieldsets = (
14
+ (None, {"fields": ("id", "mode", "is_active")}),
15
+ (
16
+ "Conteúdo",
17
+ {
18
+ "fields": ("body", "subject"),
19
+ "description": (
20
+ "Modo LOCAL: \"body\" é o texto/HTML enviado, com $variaveis "
21
+ "(ex: \"Seu código é: $code\"). "
22
+ "Modo EXTERNAL: \"body\" NÃO é texto de mensagem — é o ID do "
23
+ "template no provider. Para WhatsApp (Twilio) é o Content SID "
24
+ "(HXxxx); para SendGrid é o Dynamic Template ID (d-xxxx)."
25
+ ),
26
+ },
27
+ ),
28
+ ("Mapeamento de variáveis", {"fields": ("variable_mapping",)}),
29
+ ("Timestamps", {"fields": ("created_at", "updated_at")}),
30
+ )
31
+
32
+ @admin.display(description="body é...")
33
+ def body_kind(self, obj: NotificationTemplateModel) -> str:
34
+ if obj.mode == NotificationTemplateModel.ModeChoices.EXTERNAL:
35
+ return f"ID externo: {obj.body[:24]}"
36
+ return f"conteúdo local: {obj.body[:40]}"
@@ -51,7 +51,7 @@ class TwilioSMSProvider(NotificationProvider):
51
51
  return ProviderResult(success=True, provider_name=self.name, external_id=message.sid)
52
52
 
53
53
  except TwilioRestException as e:
54
- logger.error("Twilio SMS error", extra={"code": e.code, "msg": e.msg})
54
+ logger.error("Twilio SMS error", extra={"code": e.code, "twilio_message": e.msg})
55
55
  return ProviderResult(
56
56
  success=False, provider_name=self.name, error=f"Twilio [{e.code}]: {e.msg}"
57
57
  )
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import logging
2
3
  from typing import Any
3
4
 
@@ -99,7 +100,7 @@ class TwilioWhatsAppProvider(NotificationProvider):
99
100
  try:
100
101
  message = self._client.messages.create(
101
102
  content_sid=template.external_template_id,
102
- content_variables=str(translated), # Twilio expects JSON string
103
+ content_variables=json.dumps(translated), # Twilio expects JSON string
103
104
  from_=self._from_number,
104
105
  to=to,
105
106
  )
@@ -107,7 +108,10 @@ class TwilioWhatsAppProvider(NotificationProvider):
107
108
  success=True, provider_name=self.name, external_id=message.sid
108
109
  )
109
110
  except TwilioRestException as e:
110
- logger.error("WhatsApp external template error", extra={"code": e.code, "msg": e.msg})
111
+ logger.error(
112
+ "WhatsApp external template error",
113
+ extra={"code": e.code, "twilio_message": e.msg},
114
+ )
111
115
  return ProviderResult(
112
116
  success=False,
113
117
  provider_name=self.name,
@@ -21,6 +21,7 @@ class DjangoNotificationRepository(NotificationRepository):
21
21
  "context": notification.context,
22
22
  "status": notification.status.value,
23
23
  "provider_used": notification.provider_used,
24
+ "provider_message_id": notification.provider_message_id,
24
25
  "attempts": notification.attempts,
25
26
  "max_attempts": notification.max_attempts,
26
27
  "error_detail": notification.error_detail,
@@ -64,6 +65,7 @@ class DjangoNotificationRepository(NotificationRepository):
64
65
  created_at=obj.created_at,
65
66
  sent_at=obj.sent_at,
66
67
  provider_used=obj.provider_used,
68
+ provider_message_id=obj.provider_message_id,
67
69
  )
68
70
  n.status = NotificationStatus(obj.status)
69
71
  return n
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "notifyfork"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Provider-agnostic notification gateway for Django. Send to SMS, Email, WhatsApp, Push, and Slack via a single API."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes