fastapi-plugin-notification 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.
@@ -0,0 +1,29 @@
1
+ """FastAPI Notification Plugin
2
+
3
+ A notification plugin for FastAPI that integrates with FastAPI SDK and Postmark provider
4
+ to send notifications via email and store them in a database.
5
+ """
6
+
7
+ from fastapi_plugin_notification.app import create_notification_router
8
+ from fastapi_plugin_notification.client import NotificationClient
9
+ from fastapi_plugin_notification.controller import NotificationController
10
+ from fastapi_plugin_notification.models import NotificationModel
11
+ from fastapi_plugin_notification.schemas import (
12
+ NotificationCreate,
13
+ NotificationResponse,
14
+ NotificationResponsePaginated,
15
+ NotificationUpdate,
16
+ )
17
+
18
+ __all__ = [
19
+ "NotificationClient",
20
+ "NotificationController",
21
+ "NotificationModel",
22
+ "NotificationCreate",
23
+ "NotificationUpdate",
24
+ "NotificationResponse",
25
+ "NotificationResponsePaginated",
26
+ "create_notification_router",
27
+ ]
28
+
29
+ __version__ = "0.1.0"
@@ -0,0 +1,162 @@
1
+ """FastAPI app setup for notification plugin."""
2
+
3
+ from typing import Callable, Optional
4
+
5
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
6
+ from fastapi_sdk.security.permissions import require_permission
7
+ from odmantic import AIOEngine
8
+
9
+ from fastapi_plugin_notification.controller import NotificationController
10
+ from fastapi_plugin_notification.schemas import (
11
+ NotificationResponse,
12
+ NotificationResponsePaginated,
13
+ )
14
+
15
+
16
+ def create_notification_router(
17
+ *,
18
+ prefix: str = "/notifications",
19
+ tags: Optional[list[str]] = None,
20
+ get_db: Callable,
21
+ ) -> APIRouter:
22
+ """Create a FastAPI router for user-specific notification endpoints.
23
+
24
+ Args:
25
+ prefix: URL prefix for all routes (default: "/notifications")
26
+ tags: List of tags for API documentation (default: ["notifications"])
27
+ get_db: Database dependency function that returns AIOEngine
28
+
29
+ Returns:
30
+ Configured APIRouter instance with user-specific routes only
31
+ """
32
+ if tags is None:
33
+ tags = ["notifications"]
34
+
35
+ router = APIRouter(prefix=prefix, tags=tags)
36
+
37
+ @router.get("/me", response_model=NotificationResponsePaginated)
38
+ @require_permission("notification:read")
39
+ async def get_my_notifications(
40
+ request: Request,
41
+ page: int = Query(default=1, ge=1, description="Page number (1-based)"),
42
+ seen: Optional[bool] = Query(
43
+ default=None, description="Filter by seen status (true/false)"
44
+ ),
45
+ db: AIOEngine = Depends(get_db),
46
+ ):
47
+ """Get notifications for the current user."""
48
+ user_id = request.state.claims.get("sub")
49
+ if not user_id:
50
+ raise HTTPException(status_code=400, detail="user_id not found in claims")
51
+
52
+ controller = NotificationController(db)
53
+ query = [{"user_id": user_id}]
54
+ if seen is not None:
55
+ query.append({"seen": seen})
56
+
57
+ result = await controller.list(
58
+ page=page,
59
+ query=query,
60
+ claims=None, # No ownership rule at controller level, enforced via query
61
+ )
62
+ return result
63
+
64
+ @router.get("/me/{notification_id}", response_model=NotificationResponse)
65
+ @require_permission("notification:read")
66
+ async def get_my_notification(
67
+ request: Request,
68
+ notification_id: str,
69
+ db: AIOEngine = Depends(get_db),
70
+ ):
71
+ """Get a specific notification by ID for the current user and automatically mark it as seen."""
72
+ user_id = request.state.claims.get("sub")
73
+ if not user_id:
74
+ raise HTTPException(status_code=400, detail="user_id not found in claims")
75
+
76
+ controller = NotificationController(db)
77
+
78
+ # Get the notification
79
+ notification = await controller.get(
80
+ uuid=notification_id,
81
+ claims=None, # No ownership rule at controller level
82
+ )
83
+
84
+ if not notification:
85
+ raise HTTPException(status_code=404, detail="Notification not found")
86
+
87
+ # Enforce ownership at route level
88
+ if notification.user_id != user_id:
89
+ raise HTTPException(
90
+ status_code=403,
91
+ detail="Access denied: notification does not belong to you",
92
+ )
93
+
94
+ # Automatically mark as seen if not already seen
95
+ if not notification.seen:
96
+ notification = await controller.update(
97
+ uuid=notification_id,
98
+ data={"seen": True},
99
+ claims=None, # No ownership rule at controller level
100
+ )
101
+
102
+ return NotificationResponse(**notification.model_dump())
103
+
104
+ @router.delete("/me/{notification_id}")
105
+ @require_permission("notification:delete")
106
+ async def delete_my_notification(
107
+ request: Request,
108
+ notification_id: str,
109
+ db: AIOEngine = Depends(get_db),
110
+ ):
111
+ """Delete a notification for the current user."""
112
+ user_id = request.state.claims.get("sub")
113
+ if not user_id:
114
+ raise HTTPException(status_code=400, detail="user_id not found in claims")
115
+
116
+ controller = NotificationController(db)
117
+
118
+ # Get the notification first to check ownership
119
+ notification = await controller.get(
120
+ uuid=notification_id,
121
+ claims=None, # No ownership rule at controller level
122
+ )
123
+
124
+ if not notification:
125
+ raise HTTPException(status_code=404, detail="Notification not found")
126
+
127
+ # Enforce ownership at route level
128
+ if notification.user_id != user_id:
129
+ raise HTTPException(
130
+ status_code=403,
131
+ detail="Access denied: notification does not belong to you",
132
+ )
133
+
134
+ # Delete the notification
135
+ deleted = await controller.delete(
136
+ uuid=notification_id,
137
+ claims=None, # No ownership rule at controller level
138
+ )
139
+
140
+ if not deleted:
141
+ raise HTTPException(status_code=404, detail="Notification not found")
142
+ return {"detail": "Notification deleted"}
143
+
144
+ @router.get("/count/unseen", response_model=dict)
145
+ @require_permission("notification:read")
146
+ async def get_unseen_count(
147
+ request: Request,
148
+ db: AIOEngine = Depends(get_db),
149
+ ):
150
+ """Get count of unseen notifications for the current user."""
151
+ user_id = request.state.claims.get("sub")
152
+ if not user_id:
153
+ raise HTTPException(status_code=400, detail="user_id not found in claims")
154
+
155
+ controller = NotificationController(db)
156
+ count = await controller.count(
157
+ query=[{"user_id": user_id}, {"seen": False}],
158
+ claims=None, # No ownership rule at controller level, enforced via query
159
+ )
160
+ return {"count": count}
161
+
162
+ return router
@@ -0,0 +1,206 @@
1
+ """Notification client for sending notifications."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, Optional
5
+
6
+ from fastapi_provider_postmark import PostmarkProvider
7
+ from odmantic import AIOEngine
8
+
9
+ from fastapi_plugin_notification.controller import NotificationController
10
+ from fastapi_plugin_notification.models import NotificationModel
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class NotificationClient:
16
+ """Client for creating and sending notifications."""
17
+
18
+ def __init__(
19
+ self,
20
+ postmark_provider: PostmarkProvider,
21
+ default_template_id: Optional[int] = None,
22
+ ):
23
+ """Initialize the notification client.
24
+
25
+ Args:
26
+ postmark_provider: PostmarkProvider instance for sending emails
27
+ default_template_id: Default Postmark template ID to use for emails
28
+ """
29
+ self.postmark_provider = postmark_provider
30
+ self.default_template_id = default_template_id
31
+
32
+ async def send_notification(
33
+ self,
34
+ db_engine: AIOEngine,
35
+ name: str,
36
+ user_id: str,
37
+ user_email: str,
38
+ claims: Dict[str, Any],
39
+ metadata: Optional[Dict[str, Any]] = None,
40
+ link: Optional[str] = None,
41
+ link_name: Optional[str] = None,
42
+ template_id: Optional[int] = None,
43
+ template_variables: Optional[Dict[str, Any]] = None,
44
+ send_email: bool = True,
45
+ ) -> NotificationModel:
46
+ """Create a notification and optionally send an email.
47
+
48
+ Args:
49
+ db_engine: MongoDB engine instance from odmantic
50
+ name: Name/type of the notification
51
+ user_id: ID of the user who should receive the notification
52
+ user_email: Email address of the user
53
+ claims: User claims for ownership verification (required)
54
+ metadata: Additional metadata about the notification
55
+ link: Call to action link for the notification
56
+ link_name: Text to display on the call to action button/link in the email
57
+ template_id: Postmark template ID to use (uses default if not provided)
58
+ template_variables: Variables to pass to the email template
59
+ send_email: Whether to send an email (default: True)
60
+
61
+ Returns:
62
+ The created notification model instance
63
+ """
64
+ controller = NotificationController(db_engine)
65
+
66
+ # Prepare notification data
67
+ notification_data = {
68
+ "name": name,
69
+ "user_id": user_id,
70
+ "user_email": user_email,
71
+ "metadata": metadata or {},
72
+ "link": link,
73
+ "link_name": link_name,
74
+ "seen": False,
75
+ }
76
+
77
+ # Create notification in database
78
+ notification = await controller.create(notification_data, claims=claims)
79
+
80
+ # Send email if requested
81
+ if send_email:
82
+ try:
83
+ # Use provided template_id or default
84
+ email_template_id = template_id or self.default_template_id
85
+
86
+ if not email_template_id:
87
+ logger.warning(
88
+ "No template_id provided and no default_template_id set. "
89
+ "Skipping email send for notification %s",
90
+ notification.uuid,
91
+ )
92
+ else:
93
+ # Prepare template variables
94
+ email_variables = {
95
+ "notification_name": name,
96
+ "user_email": user_email,
97
+ "link": link or "",
98
+ "link_name": link_name or "",
99
+ **(template_variables or {}),
100
+ **(metadata or {}),
101
+ }
102
+
103
+ # Send email via Postmark
104
+ await self.postmark_provider.send_email(
105
+ to=user_email,
106
+ template_id=email_template_id,
107
+ template_variables=email_variables,
108
+ )
109
+
110
+ logger.info(
111
+ "Notification email sent successfully for notification %s to %s",
112
+ notification.uuid,
113
+ user_email,
114
+ )
115
+ except Exception as e:
116
+ # Log the error (suppress any warnings from exception handling)
117
+ try:
118
+ logger.error(
119
+ "Failed to send notification email for notification %s: %s",
120
+ notification.uuid,
121
+ str(e),
122
+ )
123
+ except Exception:
124
+ # If logging fails, just pass - don't let logging errors break notification creation
125
+ pass
126
+ # Don't fail the notification creation if email fails
127
+ # The notification is still stored in the database
128
+
129
+ return notification
130
+
131
+ async def get_user_notifications(
132
+ self,
133
+ db_engine: AIOEngine,
134
+ user_id: str,
135
+ page: int = 1,
136
+ seen: Optional[bool] = None,
137
+ claims: Optional[Dict[str, Any]] = None,
138
+ ) -> Dict[str, Any]:
139
+ """Get notifications for a specific user.
140
+
141
+ Args:
142
+ db_engine: MongoDB engine instance from odmantic
143
+ user_id: ID of the user
144
+ page: Page number (1-based)
145
+ seen: Filter by seen status (None for all, True for seen, False for unseen)
146
+ claims: Optional user claims for ownership verification
147
+
148
+ Returns:
149
+ Paginated list of notifications
150
+ """
151
+ controller = NotificationController(db_engine)
152
+ query = [{"user_id": user_id}]
153
+ if seen is not None:
154
+ query.append({"seen": seen})
155
+
156
+ return await controller.list(
157
+ page=page,
158
+ query=query,
159
+ claims=claims,
160
+ )
161
+
162
+ async def get_unseen_count(
163
+ self,
164
+ db_engine: AIOEngine,
165
+ user_id: str,
166
+ claims: Optional[Dict[str, Any]] = None,
167
+ ) -> int:
168
+ """Get count of unseen notifications for a user.
169
+
170
+ Args:
171
+ db_engine: MongoDB engine instance from odmantic
172
+ user_id: ID of the user
173
+ claims: Optional user claims for ownership verification
174
+
175
+ Returns:
176
+ Count of unseen notifications
177
+ """
178
+ controller = NotificationController(db_engine)
179
+ return await controller.count(
180
+ query=[{"user_id": user_id}, {"seen": False}],
181
+ claims=claims,
182
+ )
183
+
184
+ async def mark_as_seen(
185
+ self,
186
+ db_engine: AIOEngine,
187
+ notification_uuid: str,
188
+ claims: Optional[Dict[str, Any]] = None,
189
+ ) -> Optional[NotificationModel]:
190
+ """Mark a notification as seen.
191
+
192
+ Args:
193
+ db_engine: MongoDB engine instance from odmantic
194
+ notification_uuid: UUID of the notification
195
+ claims: Optional user claims for ownership verification
196
+
197
+ Returns:
198
+ Updated notification model instance or None if not found
199
+ """
200
+ controller = NotificationController(db_engine)
201
+ result = await controller.update(
202
+ uuid=notification_uuid,
203
+ data={"seen": True},
204
+ claims=claims,
205
+ )
206
+ return result
@@ -0,0 +1,19 @@
1
+ """Notification controller for CRUD operations."""
2
+
3
+ from fastapi_sdk.controllers import ModelController
4
+
5
+ from fastapi_plugin_notification.models import NotificationModel
6
+ from fastapi_plugin_notification.schemas import (
7
+ NotificationCreate,
8
+ NotificationResponse,
9
+ NotificationUpdate,
10
+ )
11
+
12
+
13
+ class NotificationController(ModelController):
14
+ """Controller for notification operations."""
15
+
16
+ model = NotificationModel
17
+ schema_create = NotificationCreate
18
+ schema_update = NotificationUpdate
19
+ schema_response = NotificationResponse
@@ -0,0 +1,46 @@
1
+ """Notification model for database storage."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, Optional
5
+
6
+ from fastapi_sdk.utils.model import ShortUUID, ShortUUIDType
7
+ from odmantic import Field, Index, Model
8
+
9
+
10
+ class NotificationModel(Model):
11
+ """Notification model for storing notifications in the database."""
12
+
13
+ created_at: datetime
14
+ updated_at: datetime
15
+ uuid: ShortUUIDType = Field(default_factory=lambda: ShortUUID.generate("not"))
16
+ name: str = Field(description="Name/type of the notification")
17
+ metadata: Dict[str, Any] = Field(
18
+ default_factory=dict, description="Additional metadata about the notification"
19
+ )
20
+ user_id: str = Field(
21
+ description="ID of the user who should receive the notification"
22
+ )
23
+ user_email: str = Field(
24
+ description="Email address of the user to send notification to"
25
+ )
26
+ link: Optional[str] = Field(
27
+ default=None, description="Call to action link for the notification"
28
+ )
29
+ link_name: Optional[str] = Field(
30
+ default=None,
31
+ description="Text to display on the call to action button/link in the email",
32
+ )
33
+ seen: bool = Field(
34
+ default=False, description="Whether the notification has been seen/acknowledged"
35
+ )
36
+ deleted: bool = False
37
+
38
+ model_config = {
39
+ "collection": "notification",
40
+ "indexes": lambda: [
41
+ Index(NotificationModel.uuid, unique=True),
42
+ Index(NotificationModel.user_id),
43
+ Index(NotificationModel.seen),
44
+ Index(NotificationModel.created_at),
45
+ ],
46
+ }
@@ -0,0 +1,91 @@
1
+ """Schemas for notification API requests and responses."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, Optional
5
+
6
+ from fastapi_sdk.utils.model import ShortUUIDType
7
+ from fastapi_sdk.utils.schema import BaseResponsePaginated, datetime_now_sec
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+
11
+ class NotificationBase(BaseModel):
12
+ """Base schema for common notification attributes."""
13
+
14
+ name: str = Field(
15
+ min_length=1, max_length=200, description="Name/type of the notification"
16
+ )
17
+ metadata: Dict[str, Any] = Field(
18
+ default_factory=dict, description="Additional metadata about the notification"
19
+ )
20
+ user_id: str = Field(
21
+ description="ID of the user who should receive the notification"
22
+ )
23
+ user_email: str = Field(
24
+ description="Email address of the user to send notification to"
25
+ )
26
+ link: Optional[str] = Field(
27
+ default=None, description="Call to action link for the notification"
28
+ )
29
+ link_name: Optional[str] = Field(
30
+ default=None,
31
+ description="Text to display on the call to action button/link in the email",
32
+ )
33
+
34
+
35
+ class NotificationCreate(NotificationBase):
36
+ """Schema for creating a notification."""
37
+
38
+ created_at: datetime = Field(default_factory=datetime_now_sec)
39
+ updated_at: datetime = Field(default_factory=datetime_now_sec)
40
+ seen: bool = Field(
41
+ default=False, description="Whether the notification has been seen"
42
+ )
43
+
44
+
45
+ class NotificationUpdate(BaseModel):
46
+ """Schema for updating a notification."""
47
+
48
+ name: Optional[str] = Field(
49
+ default=None,
50
+ min_length=1,
51
+ max_length=200,
52
+ description="Name/type of the notification",
53
+ )
54
+ metadata: Optional[Dict[str, Any]] = Field(
55
+ default=None, description="Additional metadata about the notification"
56
+ )
57
+ user_id: Optional[str] = Field(
58
+ default=None, description="ID of the user who should receive the notification"
59
+ )
60
+ user_email: Optional[str] = Field(
61
+ default=None, description="Email address of the user to send notification to"
62
+ )
63
+ link: Optional[str] = Field(
64
+ default=None, description="Call to action link for the notification"
65
+ )
66
+ link_name: Optional[str] = Field(
67
+ default=None,
68
+ description="Text to display on the call to action button/link in the email",
69
+ )
70
+ seen: Optional[bool] = Field(
71
+ default=None, description="Whether the notification has been seen/acknowledged"
72
+ )
73
+ updated_at: datetime = Field(default_factory=datetime_now_sec)
74
+
75
+
76
+ class NotificationResponse(NotificationBase):
77
+ """Schema for API responses."""
78
+
79
+ uuid: ShortUUIDType
80
+ created_at: datetime
81
+ updated_at: datetime
82
+ seen: bool
83
+ deleted: bool
84
+
85
+ model_config = ConfigDict(from_attributes=True)
86
+
87
+
88
+ class NotificationResponsePaginated(BaseResponsePaginated):
89
+ """Schema for paginated notification API responses."""
90
+
91
+ items: list[NotificationResponse]
@@ -0,0 +1,375 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-plugin-notification
3
+ Version: 0.1.0
4
+ Summary: Notification plugin for FastAPI projects using FastAPI SDK and Postmark provider.
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: fastapi-sdk>=0.9.7
9
+ Requires-Dist: fastapi-provider-postmark>=0.1.1
10
+ Requires-Dist: fastapi>=0.115.11
11
+ Requires-Dist: odmantic>=1.0.2
12
+ Requires-Dist: pydantic>=2.10.6
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=9.0.1; extra == "dev"
15
+ Requires-Dist: pytest-asyncio>=0.25.3; extra == "dev"
16
+ Requires-Dist: motor>=3.6.0; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # FastAPI Plugin Notification
20
+
21
+ A notification plugin for FastAPI that integrates with FastAPI SDK and Postmark provider to send notifications via email and store them in a database.
22
+
23
+ ## Features
24
+
25
+ - 📧 **Email Notifications**: Send notifications via Postmark email provider
26
+ - 💾 **Database Storage**: Store notifications in MongoDB using ODMantic
27
+ - 🔐 **User Ownership**: Automatic ownership filtering based on user claims
28
+ - 📊 **Notification Management**:
29
+ - Get all notifications for a user
30
+ - Mark notifications as seen/acknowledged
31
+ - Get unseen notification count
32
+ - 🚀 **FastAPI Integration**: Full CRUD API endpoints with authentication
33
+ - 📦 **PyPI Package**: Easy to install and use in any FastAPI project
34
+
35
+ ## Installation
36
+
37
+ Using UV:
38
+
39
+ ```bash
40
+ uv add fastapi-plugin-notification
41
+ ```
42
+
43
+ Or using pip:
44
+
45
+ ```bash
46
+ pip install fastapi-plugin-notification
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ### 1. Initialize the Notification Client
52
+
53
+ ```python
54
+ from fastapi import FastAPI, Depends
55
+ from odmantic import AIOEngine
56
+ from fastapi_provider_postmark import PostmarkProvider
57
+ from fastapi_plugin_notification import NotificationClient
58
+
59
+ app = FastAPI()
60
+
61
+ # Initialize Postmark provider
62
+ postmark = PostmarkProvider(
63
+ api_key="your-postmark-api-key",
64
+ from_email="noreply@example.com",
65
+ from_name="My App",
66
+ )
67
+
68
+ # Initialize notification client (no DB engine needed)
69
+ notification_client = NotificationClient(
70
+ postmark_provider=postmark,
71
+ default_template_id=123456, # Your Postmark template ID
72
+ )
73
+
74
+ # Dependency for notification client
75
+ def get_notification_client() -> NotificationClient:
76
+ return notification_client
77
+
78
+ # Dependency for database engine
79
+ def get_db() -> AIOEngine:
80
+ return AIOEngine(database="your_database")
81
+ ```
82
+
83
+ ### 2. Add Notification Routes to Your App
84
+
85
+ ```python
86
+ from fastapi_plugin_notification.app import create_notification_router
87
+
88
+ # Add notification routes
89
+ notification_router = create_notification_router(
90
+ prefix="/notifications",
91
+ get_db=lambda: engine,
92
+ )
93
+
94
+ app.include_router(notification_router)
95
+ ```
96
+
97
+ ### 3. Send Notifications
98
+
99
+ ```python
100
+ @app.post("/send-welcome")
101
+ async def send_welcome_notification(
102
+ user_id: str,
103
+ user_email: str,
104
+ notification_client: NotificationClient = Depends(get_notification_client),
105
+ db: AIOEngine = Depends(get_db),
106
+ ):
107
+ """Send a welcome notification to a user."""
108
+ notification = await notification_client.send_notification(
109
+ db_engine=db,
110
+ name="welcome",
111
+ user_id=user_id,
112
+ user_email=user_email,
113
+ claims=request.state.claims, # Required: user claims for ownership verification
114
+ metadata={
115
+ "welcome_message": "Welcome to our platform!",
116
+ },
117
+ link="https://example.com/dashboard",
118
+ link_name="Go to Dashboard",
119
+ template_variables={
120
+ "user_name": "John Doe",
121
+ "action_url": "https://example.com/dashboard",
122
+ },
123
+ )
124
+ return {"notification_id": notification.uuid, "message": "Notification sent"}
125
+ ```
126
+
127
+ ### 4. Get User Notifications
128
+
129
+ The plugin automatically provides these user-specific endpoints:
130
+
131
+ - `GET /notifications/me` - Get all notifications for the current user
132
+ - `GET /notifications/me?seen=false` - Get only unseen notifications
133
+ - `GET /notifications/me/{notification_id}` - Get a specific notification (automatically marks as seen)
134
+ - `DELETE /notifications/me/{notification_id}` - Delete a notification
135
+ - `GET /notifications/count/unseen` - Get count of unseen notifications
136
+
137
+ ## Usage Examples
138
+
139
+ ### Sending a Notification with Email
140
+
141
+ ```python
142
+ # In a FastAPI route handler
143
+ @app.post("/send-password-reset")
144
+ async def send_password_reset(
145
+ user_id: str,
146
+ user_email: str,
147
+ notification_client: NotificationClient = Depends(get_notification_client),
148
+ db: AIOEngine = Depends(get_db),
149
+ ):
150
+ notification = await notification_client.send_notification(
151
+ db_engine=db,
152
+ name="password_reset",
153
+ user_id=user_id,
154
+ user_email=user_email,
155
+ claims=request.state.claims, # Required: user claims for ownership verification
156
+ metadata={
157
+ "reset_token": "abc123",
158
+ "expires_in": 3600,
159
+ },
160
+ link="https://example.com/reset-password?token=abc123",
161
+ link_name="Reset Password",
162
+ template_variables={
163
+ "reset_link": "https://example.com/reset-password?token=abc123",
164
+ "expires_in_minutes": 60,
165
+ },
166
+ )
167
+ return {"notification_id": notification.uuid}
168
+ ```
169
+
170
+ ### Getting User Notifications
171
+
172
+ ```python
173
+ # Get all notifications for a user
174
+ notifications = await notification_client.get_user_notifications(
175
+ db_engine=db,
176
+ user_id="user_123",
177
+ page=1,
178
+ )
179
+
180
+ # Get only unseen notifications
181
+ unseen_notifications = await notification_client.get_user_notifications(
182
+ db_engine=db,
183
+ user_id="user_123",
184
+ page=1,
185
+ seen=False,
186
+ )
187
+
188
+ # Get unseen count
189
+ unseen_count = await notification_client.get_unseen_count(
190
+ db_engine=db,
191
+ user_id="user_123",
192
+ )
193
+ ```
194
+
195
+ ### Marking Notifications as Seen
196
+
197
+ ```python
198
+ # Mark a specific notification as seen
199
+ notification = await notification_client.mark_as_seen(
200
+ db_engine=db,
201
+ notification_uuid="not_abc123",
202
+ )
203
+ ```
204
+
205
+ ### Creating Notifications Without Email
206
+
207
+ ```python
208
+ # Create notification without sending email
209
+ notification = await notification_client.send_notification(
210
+ db_engine=db,
211
+ name="system_alert",
212
+ user_id="user_123",
213
+ user_email="user@example.com",
214
+ claims=request.state.claims, # Required: user claims for ownership verification
215
+ metadata={"alert_type": "info"},
216
+ send_email=False, # Don't send email
217
+ )
218
+ ```
219
+
220
+ ## API Endpoints
221
+
222
+ ### Get My Notifications
223
+
224
+ ```http
225
+ GET /notifications/me?page=1&seen=false
226
+ ```
227
+
228
+ **Query Parameters:**
229
+ - `page` (int): Page number (default: 1)
230
+ - `seen` (bool, optional): Filter by seen status
231
+
232
+ **Response:**
233
+ ```json
234
+ {
235
+ "items": [
236
+ {
237
+ "uuid": "not_abc123",
238
+ "name": "welcome",
239
+ "user_id": "user_123",
240
+ "user_email": "user@example.com",
241
+ "metadata": {},
242
+ "link": "https://example.com/dashboard",
243
+ "seen": false,
244
+ "created_at": "2024-01-01T00:00:00Z",
245
+ "updated_at": "2024-01-01T00:00:00Z",
246
+ "deleted": false
247
+ }
248
+ ],
249
+ "total": 1,
250
+ "page": 1,
251
+ "pages": 1,
252
+ "size": 1
253
+ }
254
+ ```
255
+
256
+ ### Get Unseen Count
257
+
258
+ ```http
259
+ GET /notifications/count/unseen
260
+ ```
261
+
262
+ **Response:**
263
+ ```json
264
+ {
265
+ "count": 5
266
+ }
267
+ ```
268
+
269
+ ### Get My Notification (Auto-marks as Seen)
270
+
271
+ ```http
272
+ GET /notifications/me/{notification_id}
273
+ ```
274
+
275
+ **Note:** This endpoint automatically marks the notification as `seen: true` when accessed. Only returns notifications belonging to the current user.
276
+
277
+ **Response:**
278
+ ```json
279
+ {
280
+ "uuid": "not_abc123",
281
+ "name": "welcome",
282
+ "user_id": "user_123",
283
+ "user_email": "user@example.com",
284
+ "metadata": {},
285
+ "link": "https://example.com/dashboard",
286
+ "link_name": "Go to Dashboard",
287
+ "seen": true,
288
+ "created_at": "2024-01-01T00:00:00Z",
289
+ "updated_at": "2024-01-01T00:00:00Z",
290
+ "deleted": false
291
+ }
292
+ ```
293
+
294
+ ## Model Structure
295
+
296
+ The `NotificationModel` includes:
297
+
298
+ - `uuid`: Unique identifier (short UUID with "not" prefix)
299
+ - `name`: Name/type of the notification
300
+ - `metadata`: Additional metadata (dict)
301
+ - `user_id`: ID of the user who should receive the notification
302
+ - `user_email`: Email address of the user
303
+ - `link`: Call to action link (optional)
304
+ - `link_name`: Text to display on the call to action button/link in the email (optional)
305
+ - `seen`: Whether the notification has been seen (default: False)
306
+ - `created_at`: Creation timestamp
307
+ - `updated_at`: Last update timestamp
308
+ - `deleted`: Soft delete flag
309
+
310
+ ## Ownership and Permissions
311
+
312
+ The plugin uses FastAPI SDK's ownership rules to ensure users can only access their own notifications. The ownership is based on the `user_id` claim in the JWT token.
313
+
314
+ Required permissions:
315
+ - `notification:create` - Create notifications
316
+ - `notification:read` - Read notifications
317
+ - `notification:update` - Update notifications
318
+ - `notification:delete` - Delete notifications
319
+
320
+ ## Configuration
321
+
322
+ ### Custom Router Configuration
323
+
324
+ ```python
325
+ from fastapi_plugin_notification.app import create_notification_router
326
+
327
+ # Create router with custom prefix and tags
328
+ notification_router = create_notification_router(
329
+ prefix="/notifications",
330
+ tags=["notifications", "alerts"],
331
+ get_db=lambda: engine,
332
+ )
333
+ ```
334
+
335
+ **Note:** All routes are user-specific and automatically filter by the current user's ID from JWT claims.
336
+
337
+ ### Custom Template Variables
338
+
339
+ The notification client automatically includes:
340
+ - `notification_name`: The name of the notification
341
+ - `user_email`: The user's email
342
+ - `link`: The notification link
343
+ - `link_name`: The text for the call to action button/link
344
+ - All `metadata` fields
345
+ - All `template_variables` fields
346
+
347
+ These are merged and passed to the Postmark template. You can use `link_name` in your email template to display custom button text for the call to action link.
348
+
349
+ ## Error Handling
350
+
351
+ The plugin handles errors gracefully:
352
+ - If email sending fails, the notification is still stored in the database
353
+ - Errors are logged but don't interrupt the notification creation process
354
+ - Database errors are propagated as HTTP exceptions
355
+
356
+ ## Development
357
+
358
+ ### Setup
359
+
360
+ 1. Clone the repository
361
+ 2. Install dependencies:
362
+ ```bash
363
+ uv sync
364
+ ```
365
+
366
+ ### Running Tests
367
+
368
+ ```bash
369
+ pytest
370
+ ```
371
+
372
+ ## License
373
+
374
+ MIT License - see LICENSE file for details
375
+
@@ -0,0 +1,11 @@
1
+ fastapi_plugin_notification/__init__.py,sha256=Com8pdasPs9LAcqsmmSf0vax0DOjZYzrvXQ5xps_1xI,895
2
+ fastapi_plugin_notification/app.py,sha256=pUku4yW7VgRH5SalRn5lT4lZZz1-3mSHYOk3oy5nUKs,5738
3
+ fastapi_plugin_notification/client.py,sha256=9Z7-sxe8J4qqKrvcgQFan_2ecdFhEA3uZhBEAZWD6_s,7268
4
+ fastapi_plugin_notification/controller.py,sha256=_IJh59F0zGg2la1yjLY-tGXKrC14dofCYgV3Bnk6CCI,547
5
+ fastapi_plugin_notification/models.py,sha256=m-UrdUshVptxIZoAQxKaVz_qAQU77EyjbRxUyASzS-E,1583
6
+ fastapi_plugin_notification/schemas.py,sha256=MyyIM0GS8u9WriX4RFsRKHxrCpAhavM1O43Dss8Waqk,2965
7
+ fastapi_plugin_notification-0.1.0.dist-info/licenses/LICENSE,sha256=deA1OnQ6dNoSO7U62Kof2Dm8w3wiJVG43wen9Ayrvv4,1071
8
+ fastapi_plugin_notification-0.1.0.dist-info/METADATA,sha256=rgkqeXvw6CWV3qnlIJhb7M8Xs9_3VTARLv95bk-dKuU,9953
9
+ fastapi_plugin_notification-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ fastapi_plugin_notification-0.1.0.dist-info/top_level.txt,sha256=ClKDla4uJBmjEnZuGLXNf3zXj0Ecz34wtRzesi3joBY,28
11
+ fastapi_plugin_notification-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Guillaume Piot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ fastapi_plugin_notification