fastapi-plugin-notification 0.2.0__tar.gz → 0.2.1__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 (22) hide show
  1. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/PKG-INFO +5 -1
  2. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/README.md +4 -0
  3. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification/__init__.py +1 -1
  4. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification/client.py +56 -0
  5. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification.egg-info/PKG-INFO +5 -1
  6. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/pyproject.toml +1 -1
  7. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/tests/test_client.py +202 -0
  8. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/LICENSE +0 -0
  9. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification/app.py +0 -0
  10. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification/controller.py +0 -0
  11. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification/models.py +0 -0
  12. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification/schemas.py +0 -0
  13. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification/types.py +0 -0
  14. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification.egg-info/SOURCES.txt +0 -0
  15. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification.egg-info/dependency_links.txt +0 -0
  16. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification.egg-info/requires.txt +0 -0
  17. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/fastapi_plugin_notification.egg-info/top_level.txt +0 -0
  18. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/setup.cfg +0 -0
  19. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/tests/test_app.py +0 -0
  20. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/tests/test_controller.py +0 -0
  21. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/tests/test_models.py +0 -0
  22. {fastapi_plugin_notification-0.2.0 → fastapi_plugin_notification-0.2.1}/tests/test_schemas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-plugin-notification
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Notification plugin for FastAPI projects using FastAPI SDK and Postmark provider.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -30,6 +30,7 @@ A notification plugin for FastAPI that integrates with FastAPI SDK and Postmark
30
30
  - Mark notifications as seen/acknowledged
31
31
  - Get unseen notification count
32
32
  - 🗞️ **Digests**: Daily or weekly digest emails, grouped by category
33
+ - 🧹 **Retention**: Configurable cleanup of notifications past their retention window
33
34
  - 🚀 **FastAPI Integration**: Full CRUD API endpoints with authentication
34
35
  - 📦 **PyPI Package**: Easy to install and use in any FastAPI project
35
36
 
@@ -78,12 +79,15 @@ Full setup — client, router and dependencies — is in the [Quick Start](docs/
78
79
  | [Quick Start](docs/quickstart.md) | Initialize the client, mount the router, send your first notification |
79
80
  | [Usage Examples](docs/usage.md) | Storing, sending, listing and acknowledging notifications |
80
81
  | [Notification Digests](docs/digests.md) | Daily/weekly digests, category grouping, example payload, Postmark Mustachio templates |
82
+ | [Cleaning Up Old Notifications](docs/cleanup.md) | Retention window and permanently removing stale notifications |
81
83
  | [API Endpoints](docs/api-endpoints.md) | HTTP routes with request parameters and response bodies |
82
84
  | [Model Structure](docs/model.md) | Fields on `NotificationModel` |
83
85
  | [Ownership and Permissions](docs/ownership.md) | Claim-based ownership and required permissions |
84
86
  | [Configuration](docs/configuration.md) | Router options, template variables, error handling |
85
87
  | [Development](docs/development.md) | Local setup and running the tests |
86
88
 
89
+ Release history is in the [Changelog](CHANGELOG.md).
90
+
87
91
  ## License
88
92
 
89
93
  MIT License - see LICENSE file for details
@@ -12,6 +12,7 @@ A notification plugin for FastAPI that integrates with FastAPI SDK and Postmark
12
12
  - Mark notifications as seen/acknowledged
13
13
  - Get unseen notification count
14
14
  - 🗞️ **Digests**: Daily or weekly digest emails, grouped by category
15
+ - 🧹 **Retention**: Configurable cleanup of notifications past their retention window
15
16
  - 🚀 **FastAPI Integration**: Full CRUD API endpoints with authentication
16
17
  - 📦 **PyPI Package**: Easy to install and use in any FastAPI project
17
18
 
@@ -60,12 +61,15 @@ Full setup — client, router and dependencies — is in the [Quick Start](docs/
60
61
  | [Quick Start](docs/quickstart.md) | Initialize the client, mount the router, send your first notification |
61
62
  | [Usage Examples](docs/usage.md) | Storing, sending, listing and acknowledging notifications |
62
63
  | [Notification Digests](docs/digests.md) | Daily/weekly digests, category grouping, example payload, Postmark Mustachio templates |
64
+ | [Cleaning Up Old Notifications](docs/cleanup.md) | Retention window and permanently removing stale notifications |
63
65
  | [API Endpoints](docs/api-endpoints.md) | HTTP routes with request parameters and response bodies |
64
66
  | [Model Structure](docs/model.md) | Fields on `NotificationModel` |
65
67
  | [Ownership and Permissions](docs/ownership.md) | Claim-based ownership and required permissions |
66
68
  | [Configuration](docs/configuration.md) | Router options, template variables, error handling |
67
69
  | [Development](docs/development.md) | Local setup and running the tests |
68
70
 
71
+ Release history is in the [Changelog](CHANGELOG.md).
72
+
69
73
  ## License
70
74
 
71
75
  MIT License - see LICENSE file for details
@@ -29,4 +29,4 @@ __all__ = [
29
29
  "create_notification_router",
30
30
  ]
31
31
 
32
- __version__ = "0.1.0"
32
+ __version__ = "0.2.1"
@@ -18,6 +18,8 @@ _DIGEST_PERIOD_DELTAS = {
18
18
  DigestPeriod.WEEK: timedelta(days=7),
19
19
  }
20
20
 
21
+ DEFAULT_RETENTION_DAYS = 30
22
+
21
23
 
22
24
  class NotificationClient:
23
25
  """Client for storing and optionally sending notifications."""
@@ -28,6 +30,7 @@ class NotificationClient:
28
30
  default_template_id: Optional[int] = None,
29
31
  default_digest_template_id: Optional[int] = None,
30
32
  digest_template_ids: Optional[Dict[str, int]] = None,
33
+ retention_days: int = DEFAULT_RETENTION_DAYS,
31
34
  ):
32
35
  """Initialize the notification client.
33
36
 
@@ -36,11 +39,14 @@ class NotificationClient:
36
39
  default_template_id: Default Postmark template ID to use for emails
37
40
  default_digest_template_id: Default Postmark template ID for digests
38
41
  digest_template_ids: Per-period digest template IDs (keys: day, week)
42
+ retention_days: Age in days after which notifications become eligible
43
+ for cleanup (default: 30)
39
44
  """
40
45
  self.postmark_provider = postmark_provider
41
46
  self.default_template_id = default_template_id
42
47
  self.default_digest_template_id = default_digest_template_id
43
48
  self.digest_template_ids = digest_template_ids or {}
49
+ self.retention_days = retention_days
44
50
 
45
51
  async def store_notification(
46
52
  self,
@@ -543,3 +549,53 @@ class NotificationClient:
543
549
  claims=claims,
544
550
  )
545
551
  return result
552
+
553
+ async def cleanup_old_notifications(
554
+ self,
555
+ db_engine: AIOEngine,
556
+ retention_days: Optional[int] = None,
557
+ include_unseen: bool = False,
558
+ dry_run: bool = False,
559
+ ) -> int:
560
+ """Permanently remove notifications older than the retention window.
561
+
562
+ Unlike the controller's soft delete, this removes documents from the
563
+ database so the collection does not grow indefinitely. Unseen
564
+ notifications are preserved unless ``include_unseen`` is set, while
565
+ soft deleted notifications are always purged once old enough.
566
+
567
+ Args:
568
+ db_engine: MongoDB engine instance from odmantic
569
+ retention_days: Age in days to keep (defaults to the client's
570
+ ``retention_days``)
571
+ include_unseen: Also remove notifications the user has not seen
572
+ dry_run: Count the matching notifications without deleting them
573
+
574
+ Returns:
575
+ Number of notifications deleted, or that would be deleted for a dry run
576
+ """
577
+ days = self.retention_days if retention_days is None else retention_days
578
+ cutoff = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=days)
579
+
580
+ query: Dict[str, Any] = {"created_at": {"$lt": cutoff}}
581
+ if not include_unseen:
582
+ query["$or"] = [{"seen": True}, {"deleted": True}]
583
+
584
+ collection = db_engine.database[NotificationModel.__collection__]
585
+
586
+ if dry_run:
587
+ count = await collection.count_documents(query)
588
+ logger.info(
589
+ "Cleanup dry run: %s notifications older than %s days would be deleted",
590
+ count,
591
+ days,
592
+ )
593
+ return count
594
+
595
+ result = await collection.delete_many(query)
596
+ logger.info(
597
+ "Deleted %s notifications older than %s days",
598
+ result.deleted_count,
599
+ days,
600
+ )
601
+ return result.deleted_count
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-plugin-notification
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Notification plugin for FastAPI projects using FastAPI SDK and Postmark provider.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -30,6 +30,7 @@ A notification plugin for FastAPI that integrates with FastAPI SDK and Postmark
30
30
  - Mark notifications as seen/acknowledged
31
31
  - Get unseen notification count
32
32
  - 🗞️ **Digests**: Daily or weekly digest emails, grouped by category
33
+ - 🧹 **Retention**: Configurable cleanup of notifications past their retention window
33
34
  - 🚀 **FastAPI Integration**: Full CRUD API endpoints with authentication
34
35
  - 📦 **PyPI Package**: Easy to install and use in any FastAPI project
35
36
 
@@ -78,12 +79,15 @@ Full setup — client, router and dependencies — is in the [Quick Start](docs/
78
79
  | [Quick Start](docs/quickstart.md) | Initialize the client, mount the router, send your first notification |
79
80
  | [Usage Examples](docs/usage.md) | Storing, sending, listing and acknowledging notifications |
80
81
  | [Notification Digests](docs/digests.md) | Daily/weekly digests, category grouping, example payload, Postmark Mustachio templates |
82
+ | [Cleaning Up Old Notifications](docs/cleanup.md) | Retention window and permanently removing stale notifications |
81
83
  | [API Endpoints](docs/api-endpoints.md) | HTTP routes with request parameters and response bodies |
82
84
  | [Model Structure](docs/model.md) | Fields on `NotificationModel` |
83
85
  | [Ownership and Permissions](docs/ownership.md) | Claim-based ownership and required permissions |
84
86
  | [Configuration](docs/configuration.md) | Router options, template variables, error handling |
85
87
  | [Development](docs/development.md) | Local setup and running the tests |
86
88
 
89
+ Release history is in the [Changelog](CHANGELOG.md).
90
+
87
91
  ## License
88
92
 
89
93
  MIT License - see LICENSE file for details
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-plugin-notification"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Notification plugin for FastAPI projects using FastAPI SDK and Postmark provider."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -722,3 +722,205 @@ class TestSendNotificationDigest:
722
722
 
723
723
  assert digest["count"] == 1
724
724
  assert digest["email_sent"] is False
725
+
726
+
727
+ class TestCleanupOldNotifications:
728
+ """Tests for cleanup_old_notifications method."""
729
+
730
+ async def _create_notification_at(
731
+ self,
732
+ notification_client,
733
+ db_engine,
734
+ mock_claims,
735
+ name: str,
736
+ created_at: datetime,
737
+ seen: bool = False,
738
+ deleted: bool = False,
739
+ ) -> NotificationModel:
740
+ notification = await notification_client.store_notification(
741
+ db_engine=db_engine,
742
+ name=name,
743
+ user_id="user_123",
744
+ user_email="user@example.com",
745
+ claims=mock_claims,
746
+ )
747
+ notification.created_at = created_at
748
+ notification.updated_at = created_at
749
+ notification.seen = seen
750
+ notification.deleted = deleted
751
+ await db_engine.save(notification)
752
+ return notification
753
+
754
+ async def _remaining_names(self, db_engine) -> set:
755
+ collection = db_engine.database["notification"]
756
+ return {doc["name"] async for doc in collection.find({})}
757
+
758
+ def test_default_retention_is_thirty_days(self, notification_client):
759
+ """Test the client defaults to a 30 day retention window."""
760
+ assert notification_client.retention_days == 30
761
+
762
+ def test_retention_days_configurable_on_init(self, postmark_provider):
763
+ """Test the retention window can be overridden when instantiating."""
764
+ client = NotificationClient(
765
+ postmark_provider=postmark_provider,
766
+ retention_days=90,
767
+ )
768
+ assert client.retention_days == 90
769
+
770
+ @pytest.mark.asyncio
771
+ async def test_cleanup_removes_old_seen_notifications(
772
+ self, notification_client, db_engine, mock_claims
773
+ ):
774
+ """Test old seen notifications are hard deleted."""
775
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
776
+
777
+ await self._create_notification_at(
778
+ notification_client,
779
+ db_engine,
780
+ mock_claims,
781
+ "old_seen",
782
+ now - timedelta(days=31),
783
+ seen=True,
784
+ )
785
+ await self._create_notification_at(
786
+ notification_client,
787
+ db_engine,
788
+ mock_claims,
789
+ "recent_seen",
790
+ now - timedelta(days=2),
791
+ seen=True,
792
+ )
793
+
794
+ deleted = await notification_client.cleanup_old_notifications(
795
+ db_engine=db_engine
796
+ )
797
+
798
+ assert deleted == 1
799
+ assert await self._remaining_names(db_engine) == {"recent_seen"}
800
+
801
+ @pytest.mark.asyncio
802
+ async def test_cleanup_preserves_old_unseen_notifications(
803
+ self, notification_client, db_engine, mock_claims
804
+ ):
805
+ """Test unseen notifications survive cleanup by default."""
806
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
807
+
808
+ await self._create_notification_at(
809
+ notification_client,
810
+ db_engine,
811
+ mock_claims,
812
+ "old_unseen",
813
+ now - timedelta(days=60),
814
+ seen=False,
815
+ )
816
+
817
+ deleted = await notification_client.cleanup_old_notifications(
818
+ db_engine=db_engine
819
+ )
820
+
821
+ assert deleted == 0
822
+ assert await self._remaining_names(db_engine) == {"old_unseen"}
823
+
824
+ @pytest.mark.asyncio
825
+ async def test_cleanup_removes_old_unseen_when_included(
826
+ self, notification_client, db_engine, mock_claims
827
+ ):
828
+ """Test include_unseen also removes old unread notifications."""
829
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
830
+
831
+ await self._create_notification_at(
832
+ notification_client,
833
+ db_engine,
834
+ mock_claims,
835
+ "old_unseen",
836
+ now - timedelta(days=60),
837
+ seen=False,
838
+ )
839
+ await self._create_notification_at(
840
+ notification_client,
841
+ db_engine,
842
+ mock_claims,
843
+ "recent_unseen",
844
+ now - timedelta(days=1),
845
+ seen=False,
846
+ )
847
+
848
+ deleted = await notification_client.cleanup_old_notifications(
849
+ db_engine=db_engine,
850
+ include_unseen=True,
851
+ )
852
+
853
+ assert deleted == 1
854
+ assert await self._remaining_names(db_engine) == {"recent_unseen"}
855
+
856
+ @pytest.mark.asyncio
857
+ async def test_cleanup_purges_old_soft_deleted_notifications(
858
+ self, notification_client, db_engine, mock_claims
859
+ ):
860
+ """Test soft deleted notifications are purged even when unseen."""
861
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
862
+
863
+ await self._create_notification_at(
864
+ notification_client,
865
+ db_engine,
866
+ mock_claims,
867
+ "old_soft_deleted",
868
+ now - timedelta(days=45),
869
+ seen=False,
870
+ deleted=True,
871
+ )
872
+
873
+ deleted = await notification_client.cleanup_old_notifications(
874
+ db_engine=db_engine
875
+ )
876
+
877
+ assert deleted == 1
878
+ assert await self._remaining_names(db_engine) == set()
879
+
880
+ @pytest.mark.asyncio
881
+ async def test_cleanup_respects_per_call_retention_days(
882
+ self, notification_client, db_engine, mock_claims
883
+ ):
884
+ """Test retention_days argument overrides the client default."""
885
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
886
+
887
+ await self._create_notification_at(
888
+ notification_client,
889
+ db_engine,
890
+ mock_claims,
891
+ "ten_days_old",
892
+ now - timedelta(days=10),
893
+ seen=True,
894
+ )
895
+
896
+ deleted = await notification_client.cleanup_old_notifications(
897
+ db_engine=db_engine,
898
+ retention_days=7,
899
+ )
900
+
901
+ assert deleted == 1
902
+ assert await self._remaining_names(db_engine) == set()
903
+
904
+ @pytest.mark.asyncio
905
+ async def test_cleanup_dry_run_reports_without_deleting(
906
+ self, notification_client, db_engine, mock_claims
907
+ ):
908
+ """Test dry_run returns the count without removing anything."""
909
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
910
+
911
+ await self._create_notification_at(
912
+ notification_client,
913
+ db_engine,
914
+ mock_claims,
915
+ "old_seen",
916
+ now - timedelta(days=31),
917
+ seen=True,
918
+ )
919
+
920
+ deleted = await notification_client.cleanup_old_notifications(
921
+ db_engine=db_engine,
922
+ dry_run=True,
923
+ )
924
+
925
+ assert deleted == 1
926
+ assert await self._remaining_names(db_engine) == {"old_seen"}