11from __future__ import annotations
22
3+ import enum
34import mimetypes
45import os
56from abc import ABC
910from bec_lib .endpoints import MessageEndpoints
1011
1112if TYPE_CHECKING :
13+ from bec_lib .connector import MessageObject
1214 from bec_lib .redis_connector import RedisConnector
1315
1416# Type variable for the message object class
@@ -126,6 +128,7 @@ class MessagingService(ABC, Generic[MessageObjectT]):
126128 def __init__ (self , redis_connector : RedisConnector ) -> None :
127129 self ._redis_connector = redis_connector
128130 self ._scopes : set [str ] = set ()
131+ self ._auto_notifications : dict [str , list [str ]] = {}
129132 self ._enabled = False
130133 self ._default_scope : str | list [str ] | None = None
131134 self ._service_config : messages .AvailableMessagingServicesMessage | None = None
@@ -134,6 +137,12 @@ def __init__(self, redis_connector: RedisConnector) -> None:
134137 cb = self ._on_new_scope_change_msg ,
135138 from_start = True ,
136139 )
140+ self ._redis_connector .register (
141+ MessageEndpoints .notification_config (), cb = self ._on_notification_config_change_msg
142+ )
143+ config_msg = self ._redis_connector .get (MessageEndpoints .notification_config ())
144+ if config_msg is not None :
145+ self ._update_auto_notifications (config_msg )
137146
138147 def set_default_scope (self , scope : str | list [str ] | None ) -> None :
139148 """
@@ -146,6 +155,133 @@ def set_default_scope(self, scope: str | list[str] | None) -> None:
146155 raise ValueError (f"Scope '{ scope } ' is not available for this messaging service." )
147156 self ._default_scope = scope
148157
158+ def set_auto_notifications (
159+ self ,
160+ event_type : Literal ["new_scan" , "scan_completed" , "alarm" , "scan_interlock" ] | str ,
161+ enabled : bool ,
162+ scopes : list [str ] | str | None = None ,
163+ ) -> None :
164+ """
165+ Set automatic notifications for a specific event type.
166+
167+ Args:
168+ event_type (Literal["new_scan", "scan_completed", "alarm", "scan_interlock"] | str): The type of event to set notifications for.
169+ enabled (bool): Whether to enable or disable notifications for the event.
170+ scopes (list[str] | str | None): The scopes to apply the notifications to.
171+ """
172+ event_name = event_type .value if isinstance (event_type , enum .Enum ) else event_type
173+ scopes_list : list [str ] = []
174+ if scopes is not None :
175+ if isinstance (scopes , str ):
176+ scopes_list = [scopes ]
177+ else :
178+ scopes_list = scopes
179+
180+ for scope in scopes_list :
181+ if scope not in self ._scopes :
182+ raise ValueError (
183+ f"Scope '{ scope } ' is not available for this messaging service."
184+ )
185+ else :
186+ if self ._default_scope is not None :
187+ scopes_list = (
188+ [self ._default_scope ]
189+ if isinstance (self ._default_scope , str )
190+ else self ._default_scope
191+ )
192+ elif not self ._SUPPORTS_EMPTY_SCOPES :
193+ raise ValueError (
194+ "Scopes must be provided when there is no default scope and empty scopes are not supported."
195+ )
196+
197+ if enabled :
198+ # merge with existing scopes if already enabled for this event type
199+ existing_scopes = set (self ._auto_notifications .get (event_name , []))
200+ existing_scopes .update (scopes_list )
201+ self ._auto_notifications [event_name ] = list (existing_scopes )
202+ else :
203+ # if disabling, remove the scopes for this event type, or the entire event type if no scopes are provided
204+ if scopes_list :
205+ existing_scopes = set (self ._auto_notifications .get (event_name , []))
206+ existing_scopes .difference_update (scopes_list )
207+ if existing_scopes :
208+ self ._auto_notifications [event_name ] = list (existing_scopes )
209+ else :
210+ self ._auto_notifications .pop (event_name , None )
211+ else :
212+ self ._auto_notifications .pop (event_name , None )
213+
214+ self ._sync_auto_notifications_config (event_name , enabled = enabled , scopes = scopes_list )
215+
216+ def _sync_auto_notifications_config (
217+ self , event_name : str , enabled : bool , scopes : list [str ]
218+ ) -> None :
219+ config_msg = self ._redis_connector .get (MessageEndpoints .notification_config ())
220+ if config_msg is None :
221+ config_msg = messages .NotificationConfigMessage ()
222+
223+ routes = {name : list (targets ) for name , targets in config_msg .routes .items ()}
224+ event_routes = list (routes .get (event_name , []))
225+
226+ if enabled :
227+ scopes_to_add = scopes or [None ]
228+ for scope in scopes_to_add :
229+ target = messages .NotificationServiceTarget (
230+ service_name = self ._SERVICE_NAME , scope = scope
231+ )
232+ if not any (existing == target for existing in event_routes ):
233+ event_routes .append (target )
234+ else :
235+ if scopes :
236+ event_routes = [
237+ target
238+ for target in event_routes
239+ if not (target .service_name == self ._SERVICE_NAME and target .scope in scopes )
240+ ]
241+ else :
242+ event_routes = [
243+ target for target in event_routes if target .service_name != self ._SERVICE_NAME
244+ ]
245+
246+ if event_routes :
247+ routes [event_name ] = event_routes
248+ else :
249+ routes .pop (event_name , None )
250+
251+ self ._redis_connector .set_and_publish (
252+ MessageEndpoints .notification_config (),
253+ messages .NotificationConfigMessage (routes = routes , metadata = config_msg .metadata ),
254+ )
255+
256+ def _on_notification_config_change_msg (
257+ self ,
258+ message : (
259+ MessageObject [messages .NotificationConfigMessage ]
260+ | dict [str , messages .NotificationConfigMessage ]
261+ ),
262+ ) -> None :
263+ config_msg = message .value if hasattr (message , "value" ) else message ["data" ]
264+ self ._update_auto_notifications (config_msg )
265+
266+ def _update_auto_notifications (self , config_msg : messages .NotificationConfigMessage ) -> None :
267+ auto_notifications : dict [str , list [str ]] = {}
268+ for event_name , targets in config_msg .routes .items ():
269+ scopes : list [str ] = []
270+ has_matching_service = False
271+ for target in targets :
272+ if target .service_name != self ._SERVICE_NAME :
273+ continue
274+ has_matching_service = True
275+ if isinstance (target .scope , str ):
276+ scopes .append (target .scope )
277+ elif isinstance (target .scope , list ):
278+ scopes .extend (target .scope )
279+
280+ if has_matching_service :
281+ auto_notifications [event_name ] = list (dict .fromkeys (scopes ))
282+
283+ self ._auto_notifications = auto_notifications
284+
149285 def _on_new_scope_change_msg (
150286 self , message : dict [str , messages .AvailableMessagingServicesMessage ]
151287 ) -> None :
0 commit comments