Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ This document provides guidelines and best practices for using GitHub Copilot in

- [Python Coding Conventions](../docs/coding-conventions.md)
- [Environment Variables Guide](../docs/env-vars.md)
- [Steps to Upgrade Python](../docs/steps-to-upgrade-python.md)
- [Pydantic Annotated fields](../docs/llm-prompts/pydantic-annotated-fields.md)
- [Steps to Upgrade Python](../docs/steps-to-upgrade-python.md)
92 changes: 92 additions & 0 deletions .github/prompts/update-user-messages.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
mode: 'edit'
description: 'Update user messages'
---

This prompt guide is for updating user-facing messages in ${file} or ${selection}

## What is a User Message?

A user message is any string that will be displayed to end-users of the application.
In our codebase, these messages are marked with the `user_message` function:

```python
from common_library.user_messages import user_message

error_msg = user_message("Operation failed. Please try again later.")
```

## Guidelines for Updating User Messages

When modifying user messages, follow these rules:

1. **Version Tracking**: Every modification to a user message must include an incremented `_version` parameter:

```python
# Before modification
user_message("Error: Unable to connect to the server.")

# After modification, add _version or increment it if it already exists
user_message("Currently unable to establish connection to the server.", _version=1)
```

2. **F-String Preservation**: When modifying messages that use f-strings, preserve all parameters and their formatting:

```python
# Before
user_message(f"Project {project_name} could not be loaded.")

# After (correct)
user_message(f"Unable to load project {project_name}.", _version=1)

# After (incorrect - lost the parameter)
user_message("Unable to load project.", _version=1)
```

3. **Message Style**: Follow *strictly* the guidelines in `${workspaceFolder}/docs/user-messages-guidelines.md`

4. **Preserve Context**: Ensure the modified message conveys the same meaning and context as the original.

5. **Incremental Versioning**: If a message already has a version, increment it by 1:

```python
# Before
user_message("Session expired.", _version=2)

# After
user_message("Your session has expired. Please log in again.", _version=3)
```

## Examples

### Example 1: Simple Message Update

```python
# Before
error_dialog(user_message("Failed to save changes."))

# After
error_dialog(user_message("Unable to save your changes. Please try again.", _version=1))
```

### Example 2: F-string Message Update

```python
# Before
raise ValueError(user_message(f"Invalid input parameter: {param_name}"))

# After
raise ValueError(user_message(f"The parameter '{param_name}' contains a value that is not allowed.", _version=1))
```

### Example 3: Already Versioned Message

```python
# Before
return HttpErrorInfo(status.HTTP_404_NOT_FOUND, user_message("User not found.", _version=1))

# After
return HttpErrorInfo(status.HTTP_404_NOT_FOUND, user_message("The requested user could not be found.", _version=2))
```

Remember: The goal is to improve clarity and helpfulness for end-users while maintaining accurate versioning for tracking changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Error and Warning Message Guidelines
# User Message Guidelines

These guidelines ensure that messages are user-friendly, clear, and helpful while maintaining a professional tone. 🚀
These guidelines ensure that error and warnings user-facing messages are user-friendly, clear, and helpful while maintaining a professional tone. 🚀

Some details:

Expand Down
11 changes: 11 additions & 0 deletions packages/common-library/src/common_library/user_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
def user_message(msg: str, *, _version: int | None = None) -> str:
"""Marks a message as user-facing
Arguments:
msg -- human-friendly string that follows docs/user-messages-guidelines.md
_version -- version number to track changes to messages; increment when modifying an existing message
Returns:
The original message string, allowing it to be used inline in code
"""
return msg
6 changes: 6 additions & 0 deletions packages/common-library/tests/test_user_messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from common_library.user_messages import user_message


def test_user_message() -> None:

assert user_message("This is a user message") == "This is a user message"
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from aiohttp.web_response import StreamResponse
from common_library.error_codes import create_error_code
from common_library.json_serialization import json_dumps, json_loads
from common_library.user_messages import user_message
from models_library.rest_error import ErrorGet, ErrorItemType, LogMessageType

from ..logging_errors import create_troubleshotting_log_kwargs
Expand All @@ -31,7 +32,7 @@
from .web_exceptions_extension import get_http_error_class_or_none

DEFAULT_API_VERSION = "v0"
_FMSG_INTERNAL_ERROR_USER_FRIENDLY = (
_FMSG_INTERNAL_ERROR_USER_FRIENDLY = user_message(
"We apologize for the inconvenience. "
"The issue has been recorded, please report it if it persists."
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from common_library.user_messages import user_message
from servicelib.aiohttp import status

from ...exception_handling import (
Expand All @@ -11,11 +12,11 @@
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {
ApiKeyDuplicatedDisplayNameError: HttpErrorInfo(
status.HTTP_409_CONFLICT,
"API key display name duplicated",
user_message("An API key with this display name already exists", _version=1),
),
ApiKeyNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND,
"API key was not found",
user_message("The requested API key could not be found", _version=1),
),
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from aiohttp import web
from common_library.error_codes import create_error_code
from common_library.user_messages import user_message
from models_library.rest_error import ErrorGet
from servicelib.aiohttp import status
from servicelib.logging_errors import create_troubleshotting_log_kwargs
Expand Down Expand Up @@ -85,22 +86,35 @@ async def _handler_catalog_client_errors(
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {
RemoteMethodNotRegisteredError: HttpErrorInfo(
status.HTTP_503_SERVICE_UNAVAILABLE,
MSG_CATALOG_SERVICE_UNAVAILABLE,
user_message(
"The catalog service is temporarily unavailable. Please try again later.",
_version=2,
),
),
CatalogForbiddenError: HttpErrorInfo(
status.HTTP_403_FORBIDDEN,
"Forbidden catalog access",
user_message(
"Access denied: You don't have permission to view this catalog item.",
_version=2,
),
),
CatalogItemNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND,
"Catalog item not found",
user_message(
"This catalog item does not exist or has been removed.", _version=2
),
),
DefaultPricingPlanNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND,
"Default pricing plan not found",
user_message(
"No default pricing plan is available for this operation.", _version=2
),
),
DefaultPricingUnitForServiceNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND, "Default pricing unit not found"
status.HTTP_404_NOT_FOUND,
user_message(
"No default pricing unit is defined for this service.", _version=2
),
),
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import Final

from common_library.user_messages import user_message
from servicelib.aiohttp.application_keys import (
APP_AIOPG_ENGINE_KEY,
APP_CONFIG_KEY,
Expand Down Expand Up @@ -42,12 +43,11 @@
"Under development. Use WEBSERVER_DEV_FEATURES_ENABLED=1 to enable current implementation"
)


# Request storage keys
RQ_PRODUCT_KEY: Final[str] = f"{__name__}.RQ_PRODUCT_KEY"


MSG_TRY_AGAIN_OR_SUPPORT: Final[str] = (
MSG_TRY_AGAIN_OR_SUPPORT: Final[str] = user_message(
"Please try again shortly. If the issue persists, contact support."
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from aiohttp import web
from common_library.error_codes import create_error_code
from common_library.user_messages import user_message
from models_library.rest_error import ErrorGet
from servicelib import status_codes_utils
from servicelib.aiohttp import status
Expand Down Expand Up @@ -43,10 +44,11 @@ async def _handler_director_service_error_as_503_or_4xx(
if status_codes_utils.is_5xx_server_error(exception.status):
# NOTE: All directorv2 5XX are mapped to 503
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
user_msg = (
user_msg = user_message(
# Most likely the director service is down or misconfigured so the user is asked to try again later.
"This service is temporarily unavailable. The incident was logged and will be investigated. "
+ MSG_TRY_AGAIN_OR_SUPPORT
"This service is temporarily unavailable. The incident has been logged and will be investigated. "
+ MSG_TRY_AGAIN_OR_SUPPORT,
_version=1,
)

# Log for further investigation
Expand Down Expand Up @@ -85,11 +87,11 @@ async def _handler_director_service_error_as_503_or_4xx(
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {
UserDefaultWalletNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND,
"Default wallet not found but necessary for computations",
user_message("Default wallet not found but necessary for computations"),
),
WalletNotEnoughCreditsError: HttpErrorInfo(
status.HTTP_402_PAYMENT_REQUIRED,
"Wallet does not have enough credits for computations. {reason}",
user_message("Wallet does not have enough credits for computations. {reason}"),
),
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging

from common_library.user_messages import user_message
from servicelib.aiohttp import status

from ...exception_handling import (
Expand Down Expand Up @@ -31,44 +32,50 @@
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {
FolderNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND,
"Folder was not found",
user_message("Folder was not found"),
),
WorkspaceNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND,
"Workspace was not found",
user_message("Workspace was not found"),
),
FolderAccessForbiddenError: HttpErrorInfo(
status.HTTP_403_FORBIDDEN,
"Does not have access to this folder",
user_message("Does not have access to this folder"),
),
WorkspaceAccessForbiddenError: HttpErrorInfo(
status.HTTP_403_FORBIDDEN,
"Does not have access to this workspace",
user_message("Does not have access to this workspace"),
),
WorkspaceFolderInconsistencyError: HttpErrorInfo(
status.HTTP_403_FORBIDDEN,
"This folder does not exist in this workspace",
user_message("This folder does not exist in this workspace"),
),
FolderValueNotPermittedError: HttpErrorInfo(
status.HTTP_409_CONFLICT,
"Provided folder value is not permitted: {reason}",
user_message("Provided folder value is not permitted: {reason}"),
),
FoldersValueError: HttpErrorInfo(
status.HTTP_409_CONFLICT,
"Invalid folder value set: {reason}",
user_message("Invalid folder value set: {reason}"),
),
ProjectInvalidRightsError: HttpErrorInfo(
status.HTTP_403_FORBIDDEN,
"Access Denied: You do not have permission to move the project with UUID: {project_uuid}. Tip: Copy and paste the UUID into the search bar to locate the project.",
user_message(
"Access Denied: You do not have permission to move the project with UUID: {project_uuid}. Tip: Copy and paste the UUID into the search bar to locate the project."
),
),
# Trashing
ProjectRunningConflictError: HttpErrorInfo(
status.HTTP_409_CONFLICT,
"One or more studies in this folder are in use and cannot be trashed. Please stop all services first and try again",
user_message(
"One or more studies in this folder are in use and cannot be trashed. Please stop all services first and try again"
),
),
ProjectStoppingError: HttpErrorInfo(
status.HTTP_503_SERVICE_UNAVAILABLE,
"Something went wrong while stopping running services in studies within this folder before trashing. Aborting trash.",
user_message(
"Something went wrong while stopping running services in studies within this folder before trashing. Aborting trash."
),
),
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging

from common_library.user_messages import user_message
from servicelib.aiohttp import status

from ...exception_handling import (
Expand All @@ -23,32 +24,32 @@
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {
UserNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND,
"User {uid} or {email} not found",
user_message("User {uid} or {email} not found"),
),
GroupNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND,
"Group {gid} not found",
user_message("Group {gid} not found"),
),
UserInGroupNotFoundError: HttpErrorInfo(
status.HTTP_404_NOT_FOUND,
"User not found in group {gid}",
user_message("User not found in group {gid}"),
),
UserAlreadyInGroupError: HttpErrorInfo(
status.HTTP_409_CONFLICT,
"User is already in group {gid}",
user_message("User is already in group {gid}"),
),
UserInsufficientRightsError: HttpErrorInfo(
status.HTTP_403_FORBIDDEN,
"Insufficient rights for {permission} access to group {gid}",
user_message("Insufficient rights for {permission} access to group {gid}"),
),
# scicrunch
InvalidRRIDError: HttpErrorInfo(
status.HTTP_409_CONFLICT,
"Invalid RRID {rrid}",
user_message("Invalid RRID {rrid}"),
),
ScicrunchError: HttpErrorInfo(
status.HTTP_409_CONFLICT,
"Cannot get RRID since scicrunch.org service is not reachable.",
user_message("Cannot get RRID since scicrunch.org service is not reachable."),
),
}

Expand Down
Loading
Loading