diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py index c0c79fdf5..f7a3e6f66 100644 --- a/backend/open_webui/constants.py +++ b/backend/open_webui/constants.py @@ -91,6 +91,19 @@ class ERROR_MESSAGES(str, Enum): INVALID_PASSWORD = lambda err='': err if err else 'The password does not meet the required validation criteria.' + AUTOMATION_LIMIT_EXCEEDED = lambda size='': f'Automation limit reached ({size})' + AUTOMATION_TOO_FREQUENT = ( + lambda interval='': f'Schedule too frequent. Minimum interval is {interval} seconds.' + ) + AUTOMATION_INVALID_RRULE = lambda err='': f'Invalid RRULE: {err}' + AUTOMATION_NO_FUTURE_RUNS = 'RRULE has no future occurrences' + + FEATURE_DISABLED = lambda name='': f'{name} is disabled' + INPUT_TOO_LONG = lambda size='': f'Input prompt exceeds maximum length of {size}' + SERVER_CONNECTION_ERROR = 'Open WebUI: Server Connection Error' + REQUIRED_FIELD_EMPTY = lambda name='': f'Required field {name} is empty' + OAUTH_NOT_CONFIGURED = lambda name='': f"Provider '{name}' is not configured" + class TASKS(str, Enum): def __str__(self) -> str: diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index cfc05160c..651e123b6 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -1131,7 +1131,7 @@ async def update_ldap_server(request: Request, form_data: LdapServerConfig, user for key in required_fields: value = getattr(form_data, key) if not value: - raise HTTPException(400, detail=f'Required field {key} is empty') + raise HTTPException(400, detail=ERROR_MESSAGES.REQUIRED_FIELD_EMPTY(key)) request.app.state.config.LDAP_SERVER_LABEL = form_data.label request.app.state.config.LDAP_SERVER_HOST = form_data.host @@ -1260,7 +1260,7 @@ async def token_exchange( if provider not in OAUTH_PROVIDERS: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"Provider '{provider}' is not configured", + detail=ERROR_MESSAGES.OAUTH_NOT_CONFIGURED(provider), ) # Get the OAuth client for this provider oauth_manager = request.app.state.oauth_manager @@ -1268,7 +1268,7 @@ async def token_exchange( if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail=f"OAuth client for '{provider}' not found", + detail=ERROR_MESSAGES.OAUTH_NOT_CONFIGURED(provider), ) # Validate the token by calling the userinfo endpoint diff --git a/backend/open_webui/routers/automations.py b/backend/open_webui/routers/automations.py index 9c532a891..d68bd8e2c 100644 --- a/backend/open_webui/routers/automations.py +++ b/backend/open_webui/routers/automations.py @@ -74,7 +74,7 @@ async def check_automation_limits(request, user, rrule_str: str, db, is_create: if max_count > 0 and await Automations.count_by_user(user.id, db=db) >= max_count: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail=f'Automation limit reached ({max_count})', + detail=ERROR_MESSAGES.AUTOMATION_LIMIT_EXCEEDED(max_count), ) # Min interval (create + update) @@ -86,7 +86,7 @@ async def check_automation_limits(request, user, rrule_str: str, db, is_create: if interval is not None and interval < min_interval: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f'Schedule too frequent. Minimum interval is {min_interval} seconds.', + detail=ERROR_MESSAGES.AUTOMATION_TOO_FREQUENT(min_interval), ) diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index b6eee93ea..a771b9592 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -140,7 +140,7 @@ async def check_channels_access(request: Request, user: Optional[UserModel] = No if not request.app.state.config.ENABLE_CHANNELS: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail='Channels are not enabled', + detail=ERROR_MESSAGES.FEATURE_DISABLED('Channels'), ) if user: @@ -1791,7 +1791,7 @@ async def post_webhook_message( if not webhook: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail='Invalid webhook URL', + detail=ERROR_MESSAGES.INVALID_URL, ) channel = await Channels.get_channel_by_id(webhook.channel_id, db=db) @@ -1809,7 +1809,7 @@ async def post_webhook_message( if not message: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail='Failed to create message', + detail=ERROR_MESSAGES.DEFAULT('Failed to create message'), ) # Update last_used_at diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index de09aa05a..1d0f0342d 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -128,7 +128,7 @@ async def load_function_from_url(request: Request, form_data: LoadUrlForm, user= 'content': data, } except Exception as e: - raise HTTPException(status_code=500, detail=f'Error importing function: {e}') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.DEFAULT(e)) ############################ diff --git a/backend/open_webui/routers/memories.py b/backend/open_webui/routers/memories.py index 3f6c080ad..cfd827481 100644 --- a/backend/open_webui/routers/memories.py +++ b/backend/open_webui/routers/memories.py @@ -268,7 +268,7 @@ async def update_memory_by_id( memory = await Memories.update_memory_by_id_and_user_id(memory_id, user.id, form_data.content) if memory is None: - raise HTTPException(status_code=404, detail='Memory not found') + raise HTTPException(status_code=404, detail=ERROR_MESSAGES.NOT_FOUND) if form_data.content is not None: vector = await request.app.state.EMBEDDING_FUNCTION(memory.content, user=user) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 0c6fe73ab..d06ceee6e 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -157,7 +157,7 @@ async def send_request( log.error(f'Failed to parse error response: {e}') raise HTTPException( status_code=r.status, - detail='Open WebUI: Server Connection Error', + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, ) r.raise_for_status() @@ -184,7 +184,7 @@ async def send_request( except Exception as e: raise HTTPException( status_code=r.status if r else 500, - detail=f'Ollama: {e}' if str(e) else 'Open WebUI: Server Connection Error', + detail=f'Ollama: {e}' if str(e) else ERROR_MESSAGES.SERVER_CONNECTION_ERROR, ) finally: if not streaming: @@ -251,7 +251,7 @@ async def verify_connection(form_data: ConnectionVerificationForm, user=Depends( return data except aiohttp.ClientError as e: log.exception(f'Client error: {str(e)}') - raise HTTPException(status_code=500, detail='Open WebUI: Server Connection Error') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) except Exception as e: log.exception(f'Unexpected error: {e}') error_detail = f'Unexpected error: {str(e)}' @@ -428,7 +428,7 @@ async def get_filtered_models(models, user, db=None): @router.get('/api/tags/{url_idx}') async def get_ollama_tags(request: Request, url_idx: Optional[int] = None, user=Depends(get_verified_user)): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) models = [] @@ -621,7 +621,7 @@ async def pull_model( user=Depends(get_admin_user), ): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) form_data = form_data.model_dump(exclude_none=True) form_data['model'] = form_data.get('model', form_data.get('name')) @@ -656,7 +656,7 @@ async def push_model( user=Depends(get_admin_user), ): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) if url_idx is None: await get_all_models(request, user=user) @@ -699,7 +699,7 @@ async def create_model( user=Depends(get_admin_user), ): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) log.debug(f'form_data: {form_data}') url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] @@ -727,7 +727,7 @@ async def copy_model( user=Depends(get_admin_user), ): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) if url_idx is None: await get_all_models(request, user=user) @@ -762,7 +762,7 @@ async def delete_model( user=Depends(get_admin_user), ): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) form_data = form_data.model_dump(exclude_none=True) form_data['model'] = form_data.get('model', form_data.get('name')) @@ -797,7 +797,7 @@ async def delete_model( @router.post('/api/show') async def show_model_info(request: Request, form_data: ModelNameForm, user=Depends(get_verified_user)): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) form_data = form_data.model_dump(exclude_none=True) form_data['model'] = form_data.get('model', form_data.get('name')) @@ -850,7 +850,7 @@ async def embed( user=Depends(get_verified_user), ): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) log.info(f'generate_ollama_batch_embeddings {form_data}') @@ -909,7 +909,7 @@ async def embeddings( user=Depends(get_verified_user), ): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) log.info(f'generate_ollama_embeddings {form_data}') @@ -976,7 +976,7 @@ async def generate_completion( user=Depends(get_verified_user), ): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) # Enforce per-model access control await check_model_access(user, await Models.get_model_by_id(form_data.model), BYPASS_MODEL_ACCESS_CONTROL) @@ -1067,7 +1067,7 @@ async def generate_chat_completion( bypass_system_prompt: bool = False, ): if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) # NOTE: We intentionally do NOT use Depends(get_async_session) here. # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. @@ -1313,7 +1313,7 @@ async def generate_anthropic_messages( See https://docs.ollama.com/api/anthropic-compatibility """ if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) payload = {**form_data} model_id = payload.get('model', '') @@ -1371,7 +1371,7 @@ async def generate_responses( See https://ollama.com/blog/responses-api """ if not request.app.state.config.ENABLE_OLLAMA_API: - raise HTTPException(status_code=503, detail='Ollama API is disabled') + raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) payload = form_data.model_dump() model_id = form_data.model @@ -1396,13 +1396,13 @@ async def generate_responses( ): raise HTTPException( status_code=403, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) else: if user.role != 'admin': raise HTTPException( status_code=403, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) url, url_idx = await get_ollama_url(request, payload['model'], url_idx) diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 09db29e6d..652c910fe 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -691,7 +691,7 @@ async def verify_connection( elif is_anthropic_url(url): result = await get_anthropic_models(url, key) if result is None: - raise HTTPException(status_code=500, detail='Failed to connect to Anthropic API') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) if 'error' in result: raise HTTPException(status_code=500, detail=result['error']) return result @@ -718,10 +718,10 @@ async def verify_connection( except aiohttp.ClientError as e: # ClientError covers all aiohttp requests issues log.exception(f'Client error: {str(e)}') - raise HTTPException(status_code=500, detail='Open WebUI: Server Connection Error') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) except Exception as e: log.exception(f'Unexpected error: {e}') - raise HTTPException(status_code=500, detail='Open WebUI: Server Connection Error') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) def get_azure_allowed_params(api_version: str) -> set[str]: @@ -1083,7 +1083,7 @@ async def generate_chat_completion( else: raise HTTPException( status_code=404, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Get the API config for the model @@ -1243,7 +1243,7 @@ async def generate_chat_completion( raise HTTPException( status_code=r.status if r else 500, - detail='Open WebUI: Server Connection Error', + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, ) finally: if not streaming: @@ -1321,7 +1321,7 @@ async def embeddings(request: Request, form_data: dict, user): log.exception(e) raise HTTPException( status_code=r.status if r else 500, - detail='Open WebUI: Server Connection Error', + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, ) finally: if not streaming: @@ -1445,7 +1445,7 @@ async def responses( log.exception(e) raise HTTPException( status_code=r.status if r else 500, - detail='Open WebUI: Server Connection Error', + detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR, ) finally: if not streaming: diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index a4a75754f..11901fc5a 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -324,7 +324,7 @@ async def update_prompt_by_id( if existing_prompt and existing_prompt.id != prompt.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Command '/{form_data.command}' is already in use by another prompt", + detail=ERROR_MESSAGES.COMMAND_TAKEN, ) form_data.access_grants = await filter_allowed_access_grants( @@ -389,7 +389,7 @@ async def update_prompt_metadata( if existing_prompt and existing_prompt.id != prompt.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Command '/{form_data.command}' is already in use", + detail=ERROR_MESSAGES.COMMAND_TAKEN, ) updated_prompt = await Prompts.update_prompt_metadata( @@ -751,7 +751,7 @@ async def get_prompt_diff( if not diff: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail='One or both history entries not found', + detail=ERROR_MESSAGES.NOT_FOUND, ) return diff diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 0bb5813e6..b921f7b3e 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -18,7 +18,7 @@ from open_webui.utils.task import ( moa_response_generation_template, ) from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.constants import TASKS +from open_webui.constants import ERROR_MESSAGES, TASKS from open_webui.routers.pipelines import process_pipeline_inlet_filter @@ -168,7 +168,7 @@ async def generate_title(request: Request, form_data: dict, user=Depends(get_ver if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -245,7 +245,7 @@ async def generate_follow_ups(request: Request, form_data: dict, user=Depends(ge if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -313,7 +313,7 @@ async def generate_chat_tags(request: Request, form_data: dict, user=Depends(get if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -375,7 +375,7 @@ async def generate_image_prompt(request: Request, form_data: dict, user=Depends( if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -431,13 +431,13 @@ async def generate_queries(request: Request, form_data: dict, user=Depends(get_v if not request.app.state.config.ENABLE_SEARCH_QUERY_GENERATION: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f'Search query generation is disabled', + detail=ERROR_MESSAGES.FEATURE_DISABLED('Search query generation'), ) elif type == 'retrieval': if not request.app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f'Query generation is disabled', + detail=ERROR_MESSAGES.FEATURE_DISABLED('Query generation'), ) if getattr(request.state, 'cached_queries', None): @@ -455,7 +455,7 @@ async def generate_queries(request: Request, form_data: dict, user=Depends(get_v if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -508,7 +508,7 @@ async def generate_autocompletion(request: Request, form_data: dict, user=Depend if not request.app.state.config.ENABLE_AUTOCOMPLETE_GENERATION: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f'Autocompletion generation is disabled', + detail=ERROR_MESSAGES.FEATURE_DISABLED('Autocompletion generation'), ) type = form_data.get('type') @@ -519,7 +519,7 @@ async def generate_autocompletion(request: Request, form_data: dict, user=Depend if len(prompt) > request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f'Input prompt exceeds maximum length of {request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH}', + detail=ERROR_MESSAGES.INPUT_TOO_LONG(request.app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH), ) if getattr(request.state, 'direct', False) and hasattr(request.state, 'model'): @@ -533,7 +533,7 @@ async def generate_autocompletion(request: Request, form_data: dict, user=Depend if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -595,7 +595,7 @@ async def generate_emoji(request: Request, form_data: dict, user=Depends(get_ver if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) # Check if the user has a custom task model @@ -661,7 +661,7 @@ async def generate_moa_response(request: Request, form_data: dict, user=Depends( if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, - detail='Model not found', + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(), ) template = DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index 041a866e3..d70b4038f 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -285,7 +285,7 @@ async def load_tool_from_url(request: Request, form_data: LoadUrlForm, user=Depe 'content': data, } except Exception as e: - raise HTTPException(status_code=500, detail=f'Error importing tool: {e}') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.DEFAULT(e)) ############################ diff --git a/backend/open_webui/routers/utils.py b/backend/open_webui/routers/utils.py index c79d8fe5d..20705c2c4 100644 --- a/backend/open_webui/routers/utils.py +++ b/backend/open_webui/routers/utils.py @@ -45,7 +45,7 @@ async def execute_code(request: Request, form_data: CodeForm, user=Depends(get_v if not request.app.state.config.ENABLE_CODE_EXECUTION: raise HTTPException( status_code=403, - detail='Code execution is disabled', + detail=ERROR_MESSAGES.FEATURE_DISABLED('Code execution'), ) if request.app.state.config.CODE_EXECUTION_ENGINE == 'jupyter': @@ -69,7 +69,7 @@ async def execute_code(request: Request, form_data: CodeForm, user=Depends(get_v else: raise HTTPException( status_code=400, - detail='Code execution engine not supported', + detail=ERROR_MESSAGES.DEFAULT('Code execution engine not supported'), ) diff --git a/backend/open_webui/utils/automations.py b/backend/open_webui/utils/automations.py index 5997b0e58..a32b04bbf 100644 --- a/backend/open_webui/utils/automations.py +++ b/backend/open_webui/utils/automations.py @@ -22,6 +22,7 @@ from dateutil.rrule import rrulestr from fastapi import Request from starlette.datastructures import Headers +from open_webui.constants import ERROR_MESSAGES from open_webui.models.automations import Automations, AutomationRuns, AutomationModel from open_webui.models.chats import ChatForm, Chats from open_webui.models.users import Users @@ -59,9 +60,9 @@ def validate_rrule(s: str) -> None: try: rule = _parse_rule(s) except Exception as e: - raise ValueError(f'Invalid RRULE: {e}') + raise ValueError(ERROR_MESSAGES.AUTOMATION_INVALID_RRULE(e)) if rule.after(datetime.now()) is None: - raise ValueError('RRULE has no future occurrences') + raise ValueError(ERROR_MESSAGES.AUTOMATION_NO_FUTURE_RUNS) def next_run_ns(s: str, tz: str = None) -> Optional[int]: