@@ -122,6 +122,38 @@ def __init__(
122122 }
123123 # --8<-- [end:channel_handlers]
124124
125+ def _validate_scheduled_time (self , scheduled_for : datetime ) -> None :
126+ """Validate that scheduled_for is in the future and within the max schedule window."""
127+ if scheduled_for < datetime .now (UTC ):
128+ raise NotificationValidationError ("scheduled_for must be in the future" )
129+ max_days = self .settings .NOTIF_MAX_SCHEDULE_DAYS
130+ max_schedule = datetime .now (UTC ) + timedelta (days = max_days )
131+ if scheduled_for > max_schedule :
132+ raise NotificationValidationError (f"scheduled_for cannot exceed { max_days } days from now" )
133+
134+ async def _check_throttle (self , user_id : str , severity : NotificationSeverity , source : str ) -> None :
135+ """Check throttle and raise NotificationThrottledError if rate limit exceeded."""
136+ if self .settings .ENVIRONMENT == "test" :
137+ return
138+ throttled = await self ._throttle_cache .check_throttle (
139+ user_id ,
140+ severity ,
141+ window_hours = self .settings .NOTIF_THROTTLE_WINDOW_HOURS ,
142+ max_per_hour = self .settings .NOTIF_THROTTLE_MAX_PER_HOUR ,
143+ )
144+ if throttled :
145+ self .logger .warning (
146+ f"Notification rate limit exceeded for user { user_id } . "
147+ f"Max { self .settings .NOTIF_THROTTLE_MAX_PER_HOUR } "
148+ f"per { self .settings .NOTIF_THROTTLE_WINDOW_HOURS } hour(s)"
149+ )
150+ self .metrics .record_notification_throttled (source )
151+ raise NotificationThrottledError (
152+ user_id ,
153+ self .settings .NOTIF_THROTTLE_MAX_PER_HOUR ,
154+ self .settings .NOTIF_THROTTLE_WINDOW_HOURS ,
155+ )
156+
125157 async def create_notification (
126158 self ,
127159 user_id : str ,
@@ -137,14 +169,7 @@ async def create_notification(
137169 if not tags :
138170 raise NotificationValidationError ("tags must be a non-empty list" )
139171 if scheduled_for is not None :
140- if scheduled_for < datetime .now (UTC ):
141- raise NotificationValidationError ("scheduled_for must be in the future" )
142- max_days = self .settings .NOTIF_MAX_SCHEDULE_DAYS
143- max_schedule = datetime .now (UTC ) + timedelta (days = max_days )
144- if scheduled_for > max_schedule :
145- raise NotificationValidationError (
146- f"scheduled_for cannot exceed { max_days } days from now"
147- )
172+ self ._validate_scheduled_time (scheduled_for )
148173 self .logger .info (
149174 f"Creating notification for user { user_id } " ,
150175 user_id = user_id ,
@@ -154,25 +179,7 @@ async def create_notification(
154179 scheduled = scheduled_for is not None ,
155180 )
156181
157- # Check throttling
158- if self .settings .ENVIRONMENT != "test" and await self ._throttle_cache .check_throttle (
159- user_id ,
160- severity ,
161- window_hours = self .settings .NOTIF_THROTTLE_WINDOW_HOURS ,
162- max_per_hour = self .settings .NOTIF_THROTTLE_MAX_PER_HOUR ,
163- ):
164- error_msg = (
165- f"Notification rate limit exceeded for user { user_id } . "
166- f"Max { self .settings .NOTIF_THROTTLE_MAX_PER_HOUR } "
167- f"per { self .settings .NOTIF_THROTTLE_WINDOW_HOURS } hour(s)"
168- )
169- self .logger .warning (error_msg )
170- self .metrics .record_notification_throttled ("general" )
171- raise NotificationThrottledError (
172- user_id ,
173- self .settings .NOTIF_THROTTLE_MAX_PER_HOUR ,
174- self .settings .NOTIF_THROTTLE_WINDOW_HOURS ,
175- )
182+ await self ._check_throttle (user_id , severity , "general" )
176183
177184 # Create notification
178185 create_data = DomainNotificationCreate (
@@ -290,13 +297,9 @@ async def _create_system_for_user(
290297 ) -> str :
291298 try :
292299 if not cfg .throttle_exempt :
293- throttled = await self ._throttle_cache .check_throttle (
294- user_id ,
295- cfg .severity ,
296- window_hours = self .settings .NOTIF_THROTTLE_WINDOW_HOURS ,
297- max_per_hour = self .settings .NOTIF_THROTTLE_MAX_PER_HOUR ,
298- )
299- if throttled :
300+ try :
301+ await self ._check_throttle (user_id , cfg .severity , "system" )
302+ except NotificationThrottledError :
300303 return "throttled"
301304
302305 await self .create_notification (
0 commit comments