notifyfork 0.1.2__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.
- notifyfork/__init__.py +4 -0
- notifyfork/api/__init__.py +0 -0
- notifyfork/api/urls.py +5 -0
- notifyfork/api/webhooks/__init__.py +0 -0
- notifyfork/api/webhooks/resend_webhook.py +127 -0
- notifyfork/api/webhooks/sendgrid_webhook.py +124 -0
- notifyfork/api/webhooks/tasks.py +74 -0
- notifyfork/api/webhooks/twilio_webhook.py +121 -0
- notifyfork/api/webhooks/urls.py +10 -0
- notifyfork/client.py +63 -0
- notifyfork/core/__init__.py +0 -0
- notifyfork/core/application/__init__.py +0 -0
- notifyfork/core/application/dtos/__init__.py +0 -0
- notifyfork/core/application/dtos/send_notification_dto.py +25 -0
- notifyfork/core/application/interfaces/__init__.py +0 -0
- notifyfork/core/application/interfaces/notification_provider.py +43 -0
- notifyfork/core/application/interfaces/notification_repository.py +15 -0
- notifyfork/core/application/interfaces/template_repository.py +8 -0
- notifyfork/core/application/use_cases/__init__.py +0 -0
- notifyfork/core/application/use_cases/send_notification.py +91 -0
- notifyfork/core/domain/__init__.py +0 -0
- notifyfork/core/domain/entities/__init__.py +0 -0
- notifyfork/core/domain/entities/notification.py +141 -0
- notifyfork/core/domain/events/__init__.py +0 -0
- notifyfork/core/domain/events/notification_events.py +26 -0
- notifyfork/core/domain/value_objects/__init__.py +0 -0
- notifyfork/core/domain/value_objects/template.py +95 -0
- notifyfork/core/infrastructure/__init__.py +0 -0
- notifyfork/core/infrastructure/apps.py +7 -0
- notifyfork/core/infrastructure/container/__init__.py +3 -0
- notifyfork/core/infrastructure/container/providers.py +218 -0
- notifyfork/core/infrastructure/migrations/0001_initial.py +48 -0
- notifyfork/core/infrastructure/migrations/0002_seed_templates.py +116 -0
- notifyfork/core/infrastructure/migrations/0003_delivery_status.py +50 -0
- notifyfork/core/infrastructure/migrations/__init__.py +0 -0
- notifyfork/core/infrastructure/models/__init__.py +6 -0
- notifyfork/core/infrastructure/models/notification_model.py +98 -0
- notifyfork/core/infrastructure/providers/__init__.py +0 -0
- notifyfork/core/infrastructure/providers/firebase_provider.py +69 -0
- notifyfork/core/infrastructure/providers/resend_provider.py +83 -0
- notifyfork/core/infrastructure/providers/sendgrid_provider.py +150 -0
- notifyfork/core/infrastructure/providers/slack_provider.py +108 -0
- notifyfork/core/infrastructure/providers/smtp_provider.py +65 -0
- notifyfork/core/infrastructure/providers/twilio_provider.py +57 -0
- notifyfork/core/infrastructure/providers/whatsapp_provider.py +135 -0
- notifyfork/core/infrastructure/queue/__init__.py +0 -0
- notifyfork/core/infrastructure/queue/tasks.py +82 -0
- notifyfork/core/infrastructure/repositories/__init__.py +0 -0
- notifyfork/core/infrastructure/repositories/notification_repository.py +69 -0
- notifyfork/core/infrastructure/repositories/template_repository.py +23 -0
- notifyfork/shared/__init__.py +0 -0
- notifyfork/shared/exceptions/__init__.py +0 -0
- notifyfork/shared/exceptions/provider_exceptions.py +21 -0
- notifyfork/shared/logging/__init__.py +0 -0
- notifyfork/shared/logging/setup.py +38 -0
- notifyfork/shared/utils/__init__.py +0 -0
- notifyfork-0.1.2.dist-info/METADATA +599 -0
- notifyfork-0.1.2.dist-info/RECORD +60 -0
- notifyfork-0.1.2.dist-info/WHEEL +4 -0
- notifyfork-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notifyfork
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Provider-agnostic notification gateway for Django. Send to SMS, Email, WhatsApp, Push, and Slack via a single API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/marioasaraujo/notifyfork
|
|
6
|
+
Project-URL: Documentation, https://github.com/marioasaraujo/notifyfork#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/marioasaraujo/notifyfork
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/marioasaraujo/notifyfork/issues
|
|
9
|
+
Author-email: Mario Araujo <marioasaraujo@gmail.com>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2024 Mario Araujo
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: celery,django,email,firebase,notifications,resend,sendgrid,slack,sms,twilio,whatsapp
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Framework :: Django
|
|
35
|
+
Classifier: Framework :: Django :: 5.0
|
|
36
|
+
Classifier: Intended Audience :: Developers
|
|
37
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Communications
|
|
41
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
42
|
+
Requires-Python: >=3.12
|
|
43
|
+
Requires-Dist: celery[redis]>=5.4
|
|
44
|
+
Requires-Dist: django>=5.0
|
|
45
|
+
Requires-Dist: djangorestframework>=3.15
|
|
46
|
+
Requires-Dist: firebase-admin>=6.5
|
|
47
|
+
Requires-Dist: httpx>=0.27
|
|
48
|
+
Requires-Dist: pydantic>=2.7
|
|
49
|
+
Requires-Dist: redis>=5.0
|
|
50
|
+
Requires-Dist: twilio>=9.0
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: hatch>=1.9; extra == 'dev'
|
|
53
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
54
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
55
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
56
|
+
Requires-Dist: pytest-django>=4.8; extra == 'dev'
|
|
57
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
58
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
59
|
+
Description-Content-Type: text/markdown
|
|
60
|
+
|
|
61
|
+
<div align="center">
|
|
62
|
+
|
|
63
|
+
<img width="100%" src="https://capsule-render.vercel.app/api?type=waving&color=0:0f2027,50:203a43,100:2c5364&height=100§ion=header&animation=fadeIn"/>
|
|
64
|
+
|
|
65
|
+
<h1>📡 NotifyFork</h1>
|
|
66
|
+
|
|
67
|
+
<p><strong>Provider-agnostic notification gateway for Django.</strong><br/>
|
|
68
|
+
One API. Any channel. Send to SMS, Email, WhatsApp, Push, and Slack,<br/>
|
|
69
|
+
delivered asynchronously, retried safely, logged in structured JSON.</p>
|
|
70
|
+
|
|
71
|
+
[](https://pypi.org/project/notifyfork)
|
|
72
|
+
[](https://python.org)
|
|
73
|
+
[](https://djangoproject.com)
|
|
74
|
+
[](https://docs.celeryq.dev)
|
|
75
|
+
[](LICENSE)
|
|
76
|
+
[](CONTRIBUTING.md)
|
|
77
|
+
|
|
78
|
+
**[English](#english) · [Português](#português)**
|
|
79
|
+
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
<a name="english"></a>
|
|
85
|
+
## 🇬🇧 English
|
|
86
|
+
|
|
87
|
+
### What is NotifyFork?
|
|
88
|
+
|
|
89
|
+
Direct Twilio calls scattered through a codebase get messy fast: duplicated retry logic, then someone needs WhatsApp and it all falls apart.
|
|
90
|
+
|
|
91
|
+
NotifyFork is a thin, provider-agnostic delivery layer. You already know the channel and template you want; it picks the right provider, enqueues the delivery, retries on failure, and logs everything as structured JSON.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install notifyfork
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import notifyfork
|
|
99
|
+
|
|
100
|
+
notifyfork.send(
|
|
101
|
+
recipient="+5511999999999",
|
|
102
|
+
channel="sms",
|
|
103
|
+
template_id="otp_sms",
|
|
104
|
+
notification_type="transactional",
|
|
105
|
+
context={"code": "847291"},
|
|
106
|
+
)
|
|
107
|
+
# → enqueued to Celery, retried on failure
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
That's the whole API. Provider selection, template rendering, and retry all happen behind the queue.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### Channels & providers
|
|
115
|
+
|
|
116
|
+
| Channel | Provider | Template mode |
|
|
117
|
+
|---------|----------|---------------|
|
|
118
|
+
| SMS | Twilio | Local (free-form text) |
|
|
119
|
+
| Email | SendGrid | Local **or** External (Dynamic Templates) |
|
|
120
|
+
| Email | Resend | Local (HTML rendered here) |
|
|
121
|
+
| Email | SMTP (any server) | Local (HTML rendered here) |
|
|
122
|
+
| WhatsApp | Twilio | Local (sandbox) **or** External (Content Templates) |
|
|
123
|
+
| Push | Firebase Cloud Messaging | Local (title + body) |
|
|
124
|
+
| Slack | Slack Web API | Local (plain or Block Kit) |
|
|
125
|
+
|
|
126
|
+
Multiple providers per channel fall back on each other automatically (see "Reliability"). Adding a provider is one new class — see "Adding a provider" below. `channel` isn't limited to this table either; you can register a provider for a channel NotifyFork doesn't ship at all.
|
|
127
|
+
|
|
128
|
+
**Generic vs. explicit `channel`**: every built-in provider accepts `channel` two ways — the generic form (`"whatsapp"`, `"email"`, `"sms"`) and its own `vendor_channel` name (`twilio_whatsapp`, `sendgrid_email`, `twilio_sms`...), because `supported_channels` lists both:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
notifyfork.send(channel="whatsapp", ...) # generic — eligible for fallback if
|
|
132
|
+
# another WhatsApp provider is registered
|
|
133
|
+
notifyfork.send(channel="twilio_whatsapp", ...) # explicit — pins Twilio, no fallback
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use generic when you want NotifyFork to pick/fall back between whichever providers are registered for that channel (ordered by `DEFAULT_PROVIDER_ORDER` / `NOTIFYFORK_PROVIDER_ORDER`, recorded in `notification.provider_used`). Use the explicit `vendor_channel` form when you specifically need that vendor — e.g. an **EXTERNAL**-mode template holds a vendor-specific template ID (a Twilio Content SID, a SendGrid Dynamic Template ID) that only the issuing provider understands, so pinning the channel documents that lock instead of implying a fallback that couldn't work anyway. `slack` is the one provider without a separate explicit form: there's only one Slack integration (the Web API), and vendor and channel are already the same word, so `slack_slack` would add nothing. See the runnable [examples](examples/) for both forms in context.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### Template modes
|
|
141
|
+
|
|
142
|
+
**LOCAL**: body is rendered here using Python's `string.Template`.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
body = "Your code is: $code"
|
|
146
|
+
context = {"code": "847291"}
|
|
147
|
+
# → "Your code is: 847291"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**EXTERNAL**: body is the provider's template ID. Variables translated via `VariableMapping` before dispatch.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
# SendGrid Dynamic Template
|
|
154
|
+
body = "d-abc123def456"
|
|
155
|
+
variable_mapping = {"name": "customer_name", "total": "order_total"}
|
|
156
|
+
|
|
157
|
+
# Twilio WhatsApp Content Template (positional)
|
|
158
|
+
body = "HXabc123def456"
|
|
159
|
+
variable_mapping = {"name": "1", "code": "2"}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### Architecture
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
notifyfork.send() (or your own view calling it)
|
|
168
|
+
│
|
|
169
|
+
▼
|
|
170
|
+
Celery Queue ← async, acks_late, exponential backoff
|
|
171
|
+
│
|
|
172
|
+
▼
|
|
173
|
+
SendNotificationUseCase
|
|
174
|
+
├── TemplateRepository ← loads template + variable mapping
|
|
175
|
+
├── ProviderRegistry ← picks provider by channel
|
|
176
|
+
└── NotificationRepository ← persists state transitions
|
|
177
|
+
|
|
178
|
+
State: PENDING → QUEUED → SENT
|
|
179
|
+
↘ RETRYING (attempt 1, 2...)
|
|
180
|
+
↘ FAILED
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The domain layer has **zero imports from providers or Django**. Swap PostgreSQL, change Celery to SQS, replace Twilio: the core logic stays untouched.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### Getting started
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pip install notifyfork
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
# settings.py
|
|
195
|
+
INSTALLED_APPS = [
|
|
196
|
+
...,
|
|
197
|
+
"notifyfork.core.infrastructure",
|
|
198
|
+
]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# urls.py (optional, only needed to receive provider delivery webhooks)
|
|
203
|
+
urlpatterns = [
|
|
204
|
+
...,
|
|
205
|
+
path("api/v1/", include("notifyfork.api.urls")),
|
|
206
|
+
]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
cp .env.example .env # fill in your provider credentials
|
|
211
|
+
python manage.py migrate
|
|
212
|
+
celery -A yourproject worker --loglevel=info # your own Celery app, autodiscovers NotifyFork's tasks
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Then try it with any of the [runnable examples](examples/) via `python manage.py shell`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### Sending a notification
|
|
220
|
+
|
|
221
|
+
**Same project**: call `notifyfork.send(...)` directly in Python, as shown above. No HTTP needed.
|
|
222
|
+
|
|
223
|
+
**Different service** (another microservice, another language): NotifyFork doesn't ship a public HTTP endpoint on purpose — auth is different per deployment, not something a library should decide for you. Add a thin authenticated view instead:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
# yourproject/notifications/views.py
|
|
227
|
+
from rest_framework.views import APIView
|
|
228
|
+
from rest_framework.response import Response
|
|
229
|
+
from rest_framework.permissions import IsAuthenticated # or whatever auth you use
|
|
230
|
+
import notifyfork
|
|
231
|
+
|
|
232
|
+
class SendNotificationView(APIView):
|
|
233
|
+
permission_classes = [IsAuthenticated]
|
|
234
|
+
|
|
235
|
+
def post(self, request):
|
|
236
|
+
task = notifyfork.send(
|
|
237
|
+
recipient=request.data["recipient"],
|
|
238
|
+
channel=request.data["channel"],
|
|
239
|
+
template_id=request.data["template_id"],
|
|
240
|
+
notification_type=request.data["notification_type"],
|
|
241
|
+
context=request.data.get("context", {}),
|
|
242
|
+
)
|
|
243
|
+
return Response({"task_id": task.id}, status=202)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Other services POST to whatever URL and auth scheme you chose — you control the contract, NotifyFork just does the delivery behind it.
|
|
247
|
+
|
|
248
|
+
The delivery-status webhooks (`notifyfork.api.webhooks`) are the one exception meant to be mounted directly: they validate the provider's own signature (Twilio, SendGrid, Resend), so they don't need your app's auth.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### Adding a provider
|
|
253
|
+
|
|
254
|
+
**Built into the lib** — subclass `NotificationProvider` and register it in `container/providers.py`:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
# notifyfork/core/infrastructure/providers/myvendor_provider.py
|
|
258
|
+
class MyVendorSMSProvider(NotificationProvider):
|
|
259
|
+
@property
|
|
260
|
+
def name(self) -> str:
|
|
261
|
+
return "myvendor_sms" # vendor_channel — see "channel vs. provider.name" above
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def supported_channels(self) -> list[NotificationChannel]:
|
|
265
|
+
return [NotificationChannel.SMS]
|
|
266
|
+
|
|
267
|
+
async def send_with_template(self, recipient, template, context) -> ProviderResult:
|
|
268
|
+
body = template.render(context)
|
|
269
|
+
# your API call here
|
|
270
|
+
...
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**From your own project** — no subclassing, no editing the container, just decorate a plain class (duck-typed, only `.name` and `.send_with_template()` are ever touched):
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
import notifyfork
|
|
277
|
+
|
|
278
|
+
@notifyfork.provider
|
|
279
|
+
class TelegramProvider:
|
|
280
|
+
name = "telegram" # vendor == channel here (like "slack"), no _channel suffix needed
|
|
281
|
+
supported_channels = ["telegram"] # channel isn't a closed enum — any string works
|
|
282
|
+
|
|
283
|
+
def supports(self, channel):
|
|
284
|
+
return channel in self.supported_channels
|
|
285
|
+
|
|
286
|
+
async def send_with_template(self, recipient, template, context):
|
|
287
|
+
...
|
|
288
|
+
|
|
289
|
+
notifyfork.send(recipient="@someone", channel="telegram", template_id="greeting", notification_type="transactional")
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The class is instantiated with no arguments and appended to `Container.providers()`. See [`examples/custom_provider`](examples/custom_provider).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### Adding a kind of notification
|
|
297
|
+
|
|
298
|
+
No event catalog to register. Pick a `channel`, write a template via migration, call `notifyfork.send(...)` with that `template_id`. Done.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### Reliability
|
|
303
|
+
|
|
304
|
+
- **Exponential backoff**: 60s → 120s → 240s, capped at 10 minutes
|
|
305
|
+
- **`acks_late=True`**: task only acknowledged after completion; safe on worker crash
|
|
306
|
+
- **Beat sweep**: periodic task re-queues notifications stuck in `RETRYING`
|
|
307
|
+
- **N+1 safe**: all queries are bounded with `LIMIT`
|
|
308
|
+
- **Provider fallback**: if you register more than one provider for the same
|
|
309
|
+
channel (e.g. SendGrid + SMTP for email), a failure on the first one falls
|
|
310
|
+
through to the next immediately, no wait for the retry backoff. Order is
|
|
311
|
+
explicit, not whatever `Container.providers()` happened to build first:
|
|
312
|
+
defaults to `DEFAULT_PROVIDER_ORDER` in `container/providers.py`, override
|
|
313
|
+
with `NOTIFYFORK_PROVIDER_ORDER=sendgrid_email,smtp_email`. Whichever
|
|
314
|
+
provider actually sent it is always recorded in `notification.provider_used`.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### Running tests
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
pytest tests/unit -v --cov=notifyfork
|
|
322
|
+
# Coverage gate: 80% minimum, see CONTRIBUTING.md
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
<a name="português"></a>
|
|
328
|
+
## 🇧🇷 Português
|
|
329
|
+
|
|
330
|
+
### O que é o NotifyFork?
|
|
331
|
+
|
|
332
|
+
Chamada direta ao Twilio espalhada pelo código vira bagunça rápido: lógica de retry duplicada, e daí alguém precisa de WhatsApp e tudo desmorona.
|
|
333
|
+
|
|
334
|
+
O NotifyFork é uma camada de entrega fina e agnóstica de provider. Você já sabe qual canal e template quer usar; ele escolhe o provider certo, enfileira o envio, faz retry em caso de falha, e loga tudo em JSON estruturado.
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
pip install notifyfork
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
import notifyfork
|
|
342
|
+
|
|
343
|
+
notifyfork.send(
|
|
344
|
+
recipient="+5511999999999",
|
|
345
|
+
channel="sms",
|
|
346
|
+
template_id="otp_sms",
|
|
347
|
+
notification_type="transactional",
|
|
348
|
+
context={"code": "847291"},
|
|
349
|
+
)
|
|
350
|
+
# → enfileirado no Celery, com retry em caso de falha
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Essa é toda a API. Seleção de provider, renderização de template e retry acontecem atrás da fila.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
### Canais e providers
|
|
358
|
+
|
|
359
|
+
| Canal | Provider | Modo de template |
|
|
360
|
+
|-------|----------|-----------------|
|
|
361
|
+
| SMS | Twilio | Local (texto livre) |
|
|
362
|
+
| E-mail | SendGrid | Local **ou** Externo (Dynamic Templates) |
|
|
363
|
+
| E-mail | Resend | Local (HTML renderizado aqui) |
|
|
364
|
+
| E-mail | SMTP (qualquer servidor) | Local (HTML renderizado aqui) |
|
|
365
|
+
| WhatsApp | Twilio | Local (sandbox) **ou** Externo (Content Templates) |
|
|
366
|
+
| Push | Firebase Cloud Messaging | Local (título + body) |
|
|
367
|
+
| Slack | Slack Web API | Local (texto simples ou Block Kit) |
|
|
368
|
+
|
|
369
|
+
Mais de um provider por canal cai um pro outro automaticamente (veja "Confiabilidade"). Adicionar um provider é uma classe nova — veja "Adicionando um provider" abaixo. `channel` também não fica preso a essa tabela; dá pra registrar um provider pra um canal que o NotifyFork nem conhece.
|
|
370
|
+
|
|
371
|
+
**`channel` genérico vs. explícito**: todo provider nativo aceita `channel` de duas formas — a genérica (`"whatsapp"`, `"email"`, `"sms"`) e o próprio nome `vendor_canal` (`twilio_whatsapp`, `sendgrid_email`, `twilio_sms`...), porque `supported_channels` lista as duas:
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
notifyfork.send(channel="whatsapp", ...) # genérico — elegível pra fallback se
|
|
375
|
+
# outro provider de WhatsApp for registrado
|
|
376
|
+
notifyfork.send(channel="twilio_whatsapp", ...) # explícito — fixa a Twilio, sem fallback
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Usa o genérico quando quer que o NotifyFork escolha/caia pro próximo entre os providers registrados pro canal (ordenado por `DEFAULT_PROVIDER_ORDER` / `NOTIFYFORK_PROVIDER_ORDER`, gravado em `notification.provider_used`). Usa a forma explícita `vendor_canal` quando precisa daquele vendor especificamente — ex: um template em modo **EXTERNO** guarda um ID específico do vendor (Content SID da Twilio, Dynamic Template ID do SendGrid) que só o provider que emitiu entende, então fixar o canal só documenta esse travamento em vez de sugerir um fallback que não funcionaria de qualquer jeito. `slack` é o único provider sem uma forma explícita separada: só existe uma integração Slack (a Web API), e vendor e canal já são a mesma palavra, então `slack_slack` não acrescentaria nada. Veja os [exemplos](examples/) rodáveis com as duas formas em contexto.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
### Modos de template
|
|
384
|
+
|
|
385
|
+
**LOCAL**: o body é renderizado aqui usando `string.Template` do Python.
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
body = "Seu código é: $code"
|
|
389
|
+
context = {"code": "847291"}
|
|
390
|
+
# → "Seu código é: 847291"
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**EXTERNO**: o body é o ID do template no provider. As variáveis são traduzidas via `VariableMapping` antes do envio.
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
# SendGrid Dynamic Template
|
|
397
|
+
body = "d-abc123def456"
|
|
398
|
+
variable_mapping = {"name": "customer_name", "total": "order_total"}
|
|
399
|
+
|
|
400
|
+
# Twilio WhatsApp Content Template (posicional)
|
|
401
|
+
body = "HXabc123def456"
|
|
402
|
+
variable_mapping = {"name": "1", "code": "2"}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### Começando
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
pip install notifyfork
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
# settings.py
|
|
415
|
+
INSTALLED_APPS = [
|
|
416
|
+
...,
|
|
417
|
+
"notifyfork.core.infrastructure",
|
|
418
|
+
]
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
# urls.py (opcional, só necessário pra receber os webhooks de entrega dos providers)
|
|
423
|
+
urlpatterns = [
|
|
424
|
+
...,
|
|
425
|
+
path("api/v1/", include("notifyfork.api.urls")),
|
|
426
|
+
]
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
cp .env.example .env # preencha as credenciais dos providers que vai usar
|
|
431
|
+
python manage.py migrate
|
|
432
|
+
celery -A seuprojeto worker --loglevel=info # seu próprio Celery, descobre as tasks do NotifyFork
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Depois é só testar com um dos [exemplos executáveis](examples/) via `python manage.py shell`.
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
### Enviando uma notificação
|
|
440
|
+
|
|
441
|
+
**Mesmo projeto**: chama `notifyfork.send(...)` direto em Python, como no exemplo acima. Sem HTTP.
|
|
442
|
+
|
|
443
|
+
**Outro serviço** (outro microserviço seu, outra linguagem): o NotifyFork propositalmente não expõe endpoint HTTP público — auth muda por deploy, não é algo que uma lib deveria decidir por você. Em vez disso, crie uma view fina e autenticada no seu próprio projeto:
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
# seuprojeto/notifications/views.py
|
|
447
|
+
from rest_framework.views import APIView
|
|
448
|
+
from rest_framework.response import Response
|
|
449
|
+
from rest_framework.permissions import IsAuthenticated # ou a auth que você usa
|
|
450
|
+
import notifyfork
|
|
451
|
+
|
|
452
|
+
class SendNotificationView(APIView):
|
|
453
|
+
permission_classes = [IsAuthenticated]
|
|
454
|
+
|
|
455
|
+
def post(self, request):
|
|
456
|
+
task = notifyfork.send(
|
|
457
|
+
recipient=request.data["recipient"],
|
|
458
|
+
channel=request.data["channel"],
|
|
459
|
+
template_id=request.data["template_id"],
|
|
460
|
+
notification_type=request.data["notification_type"],
|
|
461
|
+
context=request.data.get("context", {}),
|
|
462
|
+
)
|
|
463
|
+
return Response({"task_id": task.id}, status=202)
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Outros serviços mandam POST pra URL e esquema de auth que você escolheu — você controla o contrato, o NotifyFork só cuida do envio por trás.
|
|
467
|
+
|
|
468
|
+
Os webhooks de confirmação de entrega (`notifyfork.api.webhooks`) são a única exceção feita pra montar direto: eles validam a assinatura do próprio provider (Twilio, SendGrid, Resend), então não dependem da auth da sua aplicação.
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
### Adicionando um provider
|
|
473
|
+
|
|
474
|
+
**Dentro da lib** — herda de `NotificationProvider` e registra no `container/providers.py`:
|
|
475
|
+
|
|
476
|
+
```python
|
|
477
|
+
class MeuVendorSMSProvider(NotificationProvider):
|
|
478
|
+
@property
|
|
479
|
+
def name(self) -> str:
|
|
480
|
+
return "meuvendor_sms" # vendor_canal — veja "channel vs. provider.name" acima
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def supported_channels(self) -> list[NotificationChannel]:
|
|
484
|
+
return [NotificationChannel.SMS]
|
|
485
|
+
|
|
486
|
+
async def send_with_template(self, recipient, template, context) -> ProviderResult:
|
|
487
|
+
body = template.render(context)
|
|
488
|
+
# sua chamada de API aqui
|
|
489
|
+
...
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Do seu próprio projeto** — sem herdar nada, sem mexer no container, só decora uma classe comum (duck-typing, só `.name` e `.send_with_template()` são usados):
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
import notifyfork
|
|
496
|
+
|
|
497
|
+
@notifyfork.provider
|
|
498
|
+
class TelegramProvider:
|
|
499
|
+
name = "telegram" # vendor == canal aqui (igual "slack"), sem sufixo _canal
|
|
500
|
+
supported_channels = ["telegram"] # channel não é enum fechado, qualquer string serve
|
|
501
|
+
|
|
502
|
+
def supports(self, channel):
|
|
503
|
+
return channel in self.supported_channels
|
|
504
|
+
|
|
505
|
+
async def send_with_template(self, recipient, template, context):
|
|
506
|
+
...
|
|
507
|
+
|
|
508
|
+
notifyfork.send(recipient="@someone", channel="telegram", template_id="greeting", notification_type="transactional")
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
A classe é instanciada sem argumentos e adicionada em `Container.providers()`.
|
|
512
|
+
Veja [`examples/custom_provider`](examples/custom_provider).
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### Adicionando um tipo de notificação
|
|
517
|
+
|
|
518
|
+
Não existe catálogo de eventos. Escolhe um `channel`, cria o template via migration, chama `notifyfork.send(...)` com esse `template_id`. Pronto.
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
### Confiabilidade
|
|
523
|
+
|
|
524
|
+
- **Backoff exponencial**: 60s → 120s → 240s, limite de 10 minutos
|
|
525
|
+
- **`acks_late=True`**: task só confirmada após conclusão; seguro em caso de crash do worker
|
|
526
|
+
- **Sweep periódico**: task beat re-enfileira notificações travadas em `RETRYING`
|
|
527
|
+
- **Seguro contra N+1**: todas as queries têm `LIMIT`
|
|
528
|
+
- **Fallback entre providers**: se você registrar mais de um provider pro
|
|
529
|
+
mesmo canal (ex: SendGrid + SMTP pra email), uma falha no primeiro cai pro
|
|
530
|
+
próximo na hora, sem esperar o backoff do retry. A ordem é explícita, não é
|
|
531
|
+
"o que o `Container.providers()` montou primeiro": usa `DEFAULT_PROVIDER_ORDER`
|
|
532
|
+
em `container/providers.py` por padrão, dá pra sobrescrever com
|
|
533
|
+
`NOTIFYFORK_PROVIDER_ORDER=sendgrid_email,smtp_email`. Qual provider
|
|
534
|
+
realmente enviou fica sempre registrado em `notification.provider_used`.
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
### Rodando os testes
|
|
539
|
+
|
|
540
|
+
```bash
|
|
541
|
+
pytest tests/unit -v --cov=notifyfork
|
|
542
|
+
# Meta de cobertura: mínimo 80%, aplicado no CI
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## 🗂 Estrutura do projeto
|
|
548
|
+
|
|
549
|
+
```
|
|
550
|
+
notifyfork/
|
|
551
|
+
├── notifyfork/ ← o pacote publicado (isto que vira "pip install notifyfork")
|
|
552
|
+
│ ├── api/ ← Views Django, serializers, webhooks
|
|
553
|
+
│ └── core/
|
|
554
|
+
│ ├── domain/ ← Entidades, value objects, domain events
|
|
555
|
+
│ ├── application/ ← Use cases, interfaces, DTOs
|
|
556
|
+
│ └── infrastructure/ ← Providers, repositories, Celery tasks, container
|
|
557
|
+
├── examples/ ← Exemplos executáveis por canal
|
|
558
|
+
│ ├── sms/
|
|
559
|
+
│ ├── email/
|
|
560
|
+
│ ├── whatsapp/
|
|
561
|
+
│ ├── push/
|
|
562
|
+
│ └── slack/
|
|
563
|
+
└── tests/
|
|
564
|
+
├── conftest.py ← Fixtures compartilhadas
|
|
565
|
+
└── unit/
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## 🤝 Contributing / Contribuindo
|
|
571
|
+
|
|
572
|
+
Contributions are welcome! / Contribuições são bem-vindas!
|
|
573
|
+
|
|
574
|
+
- Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a PR
|
|
575
|
+
- Open an [issue](https://github.com/marioasaraujo/notifyfork/issues) to discuss new features or bugs
|
|
576
|
+
- PRs for new providers, bug fixes, and documentation improvements are always appreciated
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## 📬 Contact / Contato
|
|
581
|
+
|
|
582
|
+
**Mario Araujo**
|
|
583
|
+
|
|
584
|
+
[](https://linkedin.com/in/marioasaraujo)
|
|
585
|
+
[](https://github.com/marioasaraujo)
|
|
586
|
+
[](mailto:marioasaraujo@gmail.com)
|
|
587
|
+
|
|
588
|
+
> Found a bug? Open an [issue](https://github.com/marioasaraujo/notifyfork/issues).
|
|
589
|
+
> Want to collaborate or hire me for a project? Reach out on LinkedIn.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## 📄 License
|
|
594
|
+
|
|
595
|
+
MIT © [Mario Araujo](https://github.com/marioasaraujo)
|
|
596
|
+
|
|
597
|
+
Veja [LICENSE](LICENSE) para mais detalhes.
|
|
598
|
+
|
|
599
|
+
<img width="100%" src="https://capsule-render.vercel.app/api?type=waving&color=0:2c5364,50:203a43,100:0f2027&height=60§ion=footer"/>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
notifyfork/__init__.py,sha256=MPV00jqoF_LYLc7zEdQBQU5MVKIvgWH_lL9C35JJHbo,139
|
|
2
|
+
notifyfork/client.py,sha256=C6Vl7nHXl3HQzLvSpE8fYPxP7O_PGT8eSRbehaB7BCQ,1865
|
|
3
|
+
notifyfork/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
notifyfork/api/urls.py,sha256=P6MMxblenabhuccvdymJKxFpZ1x_A4XC9e_78hA2K0M,121
|
|
5
|
+
notifyfork/api/webhooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
notifyfork/api/webhooks/resend_webhook.py,sha256=Yrqi1pf30_kBh8CML_Ug2ez8iQRt2JqEM12DGlKQsS4,5200
|
|
7
|
+
notifyfork/api/webhooks/sendgrid_webhook.py,sha256=Dkm-dgtvw2B0kCnr8CSGN5IOEiKL5p7cbUlq9IU7QIA,4703
|
|
8
|
+
notifyfork/api/webhooks/tasks.py,sha256=mPBDT1qQO7dDRNECJC_9yKfuQxTDdDguiwUeIrigaS0,2439
|
|
9
|
+
notifyfork/api/webhooks/twilio_webhook.py,sha256=HTUhJFa_lkCdcegedyJfFdRirlZ_1I0GEF9wliI0S5k,4952
|
|
10
|
+
notifyfork/api/webhooks/urls.py,sha256=yyh_W7CGELOposHjRlQKiHav6DqfCGtim3BGMQ0z-84,571
|
|
11
|
+
notifyfork/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
notifyfork/core/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
notifyfork/core/application/dtos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
notifyfork/core/application/dtos/send_notification_dto.py,sha256=ul8nIETl21FwM8X6I4AG7bpeyHPtebPxkOOm_oQxORk,677
|
|
15
|
+
notifyfork/core/application/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
notifyfork/core/application/interfaces/notification_provider.py,sha256=hAGWUkGH42WxUTAZkktKHhDLUCMbfJombgkCVaupyJ8,1142
|
|
17
|
+
notifyfork/core/application/interfaces/notification_repository.py,sha256=MG70_7tkyJ7bFE_JRetipIaK8vWMMpYZrv0ZaymtI5Q,460
|
|
18
|
+
notifyfork/core/application/interfaces/template_repository.py,sha256=BnWm8KupfF3ygjpMqvTmIU9WFbqyCZTJQK-GMVu9Ipg,253
|
|
19
|
+
notifyfork/core/application/use_cases/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
notifyfork/core/application/use_cases/send_notification.py,sha256=TFx7IB67jDrqMTU2oYfLHz7JLIWXS2gcnR5PUpG2Uoo,3508
|
|
21
|
+
notifyfork/core/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
notifyfork/core/domain/entities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
notifyfork/core/domain/entities/notification.py,sha256=AiVCylpgk2u2ZPRp5L63ozSJHNXJVbCTWQ-I1tVpJKI,5374
|
|
24
|
+
notifyfork/core/domain/events/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
notifyfork/core/domain/events/notification_events.py,sha256=gH2b4OfkWG0P3TrkOn2N-21aWwlCDf_juTx7U5MIxDg,614
|
|
26
|
+
notifyfork/core/domain/value_objects/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
notifyfork/core/domain/value_objects/template.py,sha256=39y9KMqErsHCciDVAIP3AhYw0INgpumqX650LQjFVpM,3503
|
|
28
|
+
notifyfork/core/infrastructure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
notifyfork/core/infrastructure/apps.py,sha256=sqUmVWP76EsNo132KzcEfFXNtvB62z4cEmJd6KXabKs,201
|
|
30
|
+
notifyfork/core/infrastructure/container/__init__.py,sha256=17xoeeFxTi0qxGUDnIwMRHFYcRzpfU6EdmnMw3Ml60Y,98
|
|
31
|
+
notifyfork/core/infrastructure/container/providers.py,sha256=sMPGzFeR7gpME3aPlGL6-1HYc-ih8FoBLeaFVFc__8I,8480
|
|
32
|
+
notifyfork/core/infrastructure/migrations/0001_initial.py,sha256=-JXkIRhj6xicO-hbs6-kVg_97qbLDtmnEWkaRS9CJlA,2259
|
|
33
|
+
notifyfork/core/infrastructure/migrations/0002_seed_templates.py,sha256=tj1p04arZpE9aVncX85s3S3UMszAcGUrcaSHJDNRzUU,3704
|
|
34
|
+
notifyfork/core/infrastructure/migrations/0003_delivery_status.py,sha256=fe1XamOLTs1pETuHrZMhu2dC0oF_TNA1NnUDyaiTGCs,1710
|
|
35
|
+
notifyfork/core/infrastructure/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
+
notifyfork/core/infrastructure/models/__init__.py,sha256=cqzcYMLNrjiONxqi_y73r_palZ2ohkxBr6E6nygrRK0,189
|
|
37
|
+
notifyfork/core/infrastructure/models/notification_model.py,sha256=PXOJpOed8yF3KTM6s8mfMgLia2w_BMj_TtXXDbfRQCw,3625
|
|
38
|
+
notifyfork/core/infrastructure/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
+
notifyfork/core/infrastructure/providers/firebase_provider.py,sha256=QHHIhtMruIspHvP4-zUfuGmZiCmCqHxy3YwOLQ3Ds4Q,2224
|
|
40
|
+
notifyfork/core/infrastructure/providers/resend_provider.py,sha256=sdph3OKX5oiTCFNOE_U8VIic6FOLOVg_Ya9eX4ONV9s,3016
|
|
41
|
+
notifyfork/core/infrastructure/providers/sendgrid_provider.py,sha256=iPj_WMFo_9DitMbWEJuWDYxQ0d_iPQ_FdBuD3g1MfSM,5622
|
|
42
|
+
notifyfork/core/infrastructure/providers/slack_provider.py,sha256=1ZKnYeRsQntsH0XyCnMabdtT69kFOvLF1RbhX3sNlHQ,3530
|
|
43
|
+
notifyfork/core/infrastructure/providers/smtp_provider.py,sha256=BtNdEtmVBGbSeG04W_MNdkGkxERjH71wWpZgc9AgJ5M,2423
|
|
44
|
+
notifyfork/core/infrastructure/providers/twilio_provider.py,sha256=BrsfOqtTlYvhtrkdleG3J5hDd2B8mQ8iJFQmv7A7v78,2053
|
|
45
|
+
notifyfork/core/infrastructure/providers/whatsapp_provider.py,sha256=FwfjEVYZLg6sVMWeJvbmYihuS6amuCsZlft_FC8XqmM,5479
|
|
46
|
+
notifyfork/core/infrastructure/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
|
+
notifyfork/core/infrastructure/queue/tasks.py,sha256=pApZVvivuKFeQ5BLWs6XoNmwotrpbfPeHgEhZXURSqc,2785
|
|
48
|
+
notifyfork/core/infrastructure/repositories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
|
+
notifyfork/core/infrastructure/repositories/notification_repository.py,sha256=K-nfImI3PBj4pyz-TWtp2GCs7M9aRBNBMO0FnPYnm5M,2796
|
|
50
|
+
notifyfork/core/infrastructure/repositories/template_repository.py,sha256=JZ9KotPvZN9B_nQQdebA-BrhUZk-8Rel-qAqndv3vlE,1038
|
|
51
|
+
notifyfork/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
+
notifyfork/shared/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
|
+
notifyfork/shared/exceptions/provider_exceptions.py,sha256=lwCqpHMNqTubvmoyuQnXMhzXw-_0-JJiHYzbfxg152U,763
|
|
54
|
+
notifyfork/shared/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
+
notifyfork/shared/logging/setup.py,sha256=bkm2SJPifTF4IkWMq_vuLdnJrq6O5Rtq58FnWbOcKb0,1099
|
|
56
|
+
notifyfork/shared/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
|
+
notifyfork-0.1.2.dist-info/METADATA,sha256=uomRXCuPNhgCNFS3U0T43tbF4at5__nVl2fDUJplDPU,23753
|
|
58
|
+
notifyfork-0.1.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
59
|
+
notifyfork-0.1.2.dist-info/licenses/LICENSE,sha256=RuRSYCysHE6bSxocKxlVfKTutkE0pCl-H8-pEhmk468,1069
|
|
60
|
+
notifyfork-0.1.2.dist-info/RECORD,,
|