Skip to content

Commit a8b6bec

Browse files
committed
Merge branch 'master' into 1922-ensure-no-hang-in-api-server
2 parents b39ecf0 + a21f916 commit a8b6bec

File tree

51 files changed

+620
-249
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+620
-249
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,5 @@ This document provides guidelines and best practices for using GitHub Copilot in
5151

5252
- [Python Coding Conventions](../docs/coding-conventions.md)
5353
- [Environment Variables Guide](../docs/env-vars.md)
54-
- [Steps to Upgrade Python](../docs/steps-to-upgrade-python.md)
5554
- [Pydantic Annotated fields](../docs/llm-prompts/pydantic-annotated-fields.md)
55+
- [Steps to Upgrade Python](../docs/steps-to-upgrade-python.md)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
mode: 'edit'
3+
description: 'Update user messages'
4+
---
5+
6+
This prompt guide is for updating user-facing messages in ${file} or ${selection}
7+
8+
## What is a User Message?
9+
10+
A user message is any string that will be displayed to end-users of the application.
11+
In our codebase, these messages are marked with the `user_message` function:
12+
13+
```python
14+
from common_library.user_messages import user_message
15+
16+
error_msg = user_message("Operation failed. Please try again later.")
17+
```
18+
19+
## Guidelines for Updating User Messages
20+
21+
When modifying user messages, follow these rules:
22+
23+
1. **Version Tracking**: Every modification to a user message must include an incremented `_version` parameter:
24+
25+
```python
26+
# Before modification
27+
user_message("Error: Unable to connect to the server.")
28+
29+
# After modification, add _version or increment it if it already exists
30+
user_message("Currently unable to establish connection to the server.", _version=1)
31+
```
32+
33+
2. **F-String Preservation**: When modifying messages that use f-strings, preserve all parameters and their formatting:
34+
35+
```python
36+
# Before
37+
user_message(f"Project {project_name} could not be loaded.")
38+
39+
# After (correct)
40+
user_message(f"Unable to load project {project_name}.", _version=1)
41+
42+
# After (incorrect - lost the parameter)
43+
user_message("Unable to load project.", _version=1)
44+
```
45+
46+
3. **Message Style**: Follow *strictly* the guidelines in `${workspaceFolder}/docs/user-messages-guidelines.md`
47+
48+
4. **Preserve Context**: Ensure the modified message conveys the same meaning and context as the original.
49+
50+
5. **Incremental Versioning**: If a message already has a version, increment it by 1:
51+
52+
```python
53+
# Before
54+
user_message("Session expired.", _version=2)
55+
56+
# After
57+
user_message("Your session has expired. Please log in again.", _version=3)
58+
```
59+
60+
## Examples
61+
62+
### Example 1: Simple Message Update
63+
64+
```python
65+
# Before
66+
error_dialog(user_message("Failed to save changes."))
67+
68+
# After
69+
error_dialog(user_message("Unable to save your changes. Please try again.", _version=1))
70+
```
71+
72+
### Example 2: F-string Message Update
73+
74+
```python
75+
# Before
76+
raise ValueError(user_message(f"Invalid input parameter: {param_name}"))
77+
78+
# After
79+
raise ValueError(user_message(f"The parameter '{param_name}' contains a value that is not allowed.", _version=1))
80+
```
81+
82+
### Example 3: Already Versioned Message
83+
84+
```python
85+
# Before
86+
return HttpErrorInfo(status.HTTP_404_NOT_FOUND, user_message("User not found.", _version=1))
87+
88+
# After
89+
return HttpErrorInfo(status.HTTP_404_NOT_FOUND, user_message("The requested user could not be found.", _version=2))
90+
```
91+
92+
Remember: The goal is to improve clarity and helpfulness for end-users while maintaining accurate versioning for tracking changes.

docs/messages-guidelines.md renamed to docs/user-messages-guidelines.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Error and Warning Message Guidelines
1+
# User Message Guidelines
22

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

55
Some details:
66

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
def user_message(msg: str, *, _version: int | None = None) -> str:
2+
"""Marks a message as user-facing
3+
4+
Arguments:
5+
msg -- human-friendly string that follows docs/user-messages-guidelines.md
6+
_version -- version number to track changes to messages; increment when modifying an existing message
7+
8+
Returns:
9+
The original message string, allowing it to be used inline in code
10+
"""
11+
return msg
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from common_library.user_messages import user_message
2+
3+
4+
def test_user_message() -> None:
5+
6+
assert user_message("This is a user message") == "This is a user message"

packages/models-library/src/models_library/projects.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
from uuid import UUID
99

1010
from common_library.basic_types import DEFAULT_FACTORY
11-
from models_library.basic_types import ConstrainedStr
12-
from models_library.folders import FolderID
13-
from models_library.workspaces import WorkspaceID
1411
from pydantic import (
1512
BaseModel,
1613
ConfigDict,
@@ -21,8 +18,11 @@
2118
)
2219

2320
from .basic_regex import DATE_RE, UUID_RE_BASE
21+
from .basic_types import ConstrainedStr
2422
from .emails import LowerCaseEmailStr
23+
from .folders import FolderID
2524
from .groups import GroupID
25+
from .products import ProductName
2626
from .projects_access import AccessRights, GroupIDStr
2727
from .projects_nodes import Node
2828
from .projects_nodes_io import NodeIDStr
@@ -33,6 +33,7 @@
3333
none_to_empty_str_pre_validator,
3434
)
3535
from .utils.enums import StrAutoEnum
36+
from .workspaces import WorkspaceID
3637

3738
ProjectID: TypeAlias = UUID
3839
CommitID: TypeAlias = int
@@ -147,6 +148,25 @@ def _convert_sql_alchemy_enum(cls, v):
147148
)
148149

149150

151+
class ProjectListAtDB(BaseProjectModel):
152+
id: int
153+
type: ProjectType
154+
template_type: ProjectTemplateType | None
155+
prj_owner: int | None
156+
ui: dict[str, Any] | None
157+
classifiers: list[ClassifierID] | None
158+
dev: dict[str, Any] | None
159+
quality: dict[str, Any]
160+
published: bool | None
161+
hidden: bool
162+
workspace_id: WorkspaceID | None
163+
trashed: datetime | None
164+
trashed_by: UserID | None
165+
trashed_explicitly: bool
166+
product_name: ProductName
167+
folder_id: FolderID | None
168+
169+
150170
class Project(BaseProjectModel):
151171
# NOTE: This is the pydantic pendant of project-v0.0.1.json used in the API of the webserver/webclient
152172
# NOT for usage with DB!!

packages/models-library/src/models_library/projects_nodes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class Node(BaseModel):
168168
),
169169
] = None
170170

171-
thumbnail: Annotated[
171+
thumbnail: Annotated[ # <-- (DEPRECATED) Can be removed
172172
str | HttpUrl | None,
173173
Field(
174174
description="url of the latest screenshot of the node",
@@ -232,18 +232,18 @@ class Node(BaseModel):
232232
] = DEFAULT_FACTORY
233233

234234
output_node: Annotated[bool | None, Field(deprecated=True, alias="outputNode")] = (
235-
None
235+
None # <-- (DEPRECATED) Can be removed
236236
)
237237

238-
output_nodes: Annotated[
238+
output_nodes: Annotated[ # <-- (DEPRECATED) Can be removed
239239
list[NodeID] | None,
240240
Field(
241241
description="Used in group-nodes. Node IDs of those connected to the output",
242242
alias="outputNodes",
243243
),
244244
] = None
245245

246-
parent: Annotated[
246+
parent: Annotated[ # <-- (DEPRECATED) Can be removed
247247
NodeID | None,
248248
Field(
249249
description="Parent's (group-nodes') node ID s. Used to group",

packages/service-library/src/servicelib/aiohttp/rest_middlewares.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from aiohttp.web_response import StreamResponse
1414
from common_library.error_codes import create_error_code
1515
from common_library.json_serialization import json_dumps, json_loads
16+
from common_library.user_messages import user_message
1617
from models_library.rest_error import ErrorGet, ErrorItemType, LogMessageType
1718

1819
from ..logging_errors import create_troubleshotting_log_kwargs
@@ -31,7 +32,7 @@
3132
from .web_exceptions_extension import get_http_error_class_or_none
3233

3334
DEFAULT_API_VERSION = "v0"
34-
_FMSG_INTERNAL_ERROR_USER_FRIENDLY = (
35+
_FMSG_INTERNAL_ERROR_USER_FRIENDLY = user_message(
3536
"We apologize for the inconvenience. "
3637
"The issue has been recorded, please report it if it persists."
3738
)

packages/service-library/src/servicelib/fastapi/long_running_tasks/_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def __init__(self, app: FastAPI, async_client: AsyncClient, base_url: str):
120120
"""
121121
self.app = app
122122
self._async_client = async_client
123-
self._base_url = base_url
123+
self.base_url = base_url
124124

125125
@property
126126
def _client_configuration(self) -> ClientConfiguration:
@@ -129,7 +129,7 @@ def _client_configuration(self) -> ClientConfiguration:
129129

130130
def _get_url(self, path: str) -> str:
131131
url_path = f"{self._client_configuration.router_prefix}{path}".lstrip("/")
132-
url = TypeAdapter(AnyHttpUrl).validate_python(f"{self._base_url}{url_path}")
132+
url = TypeAdapter(AnyHttpUrl).validate_python(f"{self.base_url}{url_path}")
133133
return f"{url}"
134134

135135
@retry_on_http_errors

packages/service-library/src/servicelib/fastapi/long_running_tasks/_context_manager.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import asyncio
2-
from asyncio.log import logger
2+
import logging
33
from collections.abc import AsyncIterator
44
from contextlib import asynccontextmanager
55
from typing import Any, Final
66

77
from pydantic import PositiveFloat
8+
from servicelib.logging_errors import create_troubleshotting_log_message
89

9-
from ...long_running_tasks.errors import TaskClientTimeoutError
10+
from ...long_running_tasks.errors import TaskClientTimeoutError, TaskExceptionError
1011
from ...long_running_tasks.models import (
1112
ProgressCallback,
1213
ProgressMessage,
@@ -16,6 +17,8 @@
1617
)
1718
from ._client import Client
1819

20+
_logger = logging.getLogger(__name__)
21+
1922
# NOTE: very short running requests are involved
2023
MAX_CONCURRENCY: Final[int] = 10
2124

@@ -96,7 +99,7 @@ async def periodic_task_result(
9699

97100
async def _status_update() -> TaskStatus:
98101
task_status: TaskStatus = await client.get_task_status(task_id)
99-
logger.debug("Task status %s", task_status.model_dump_json())
102+
_logger.debug("Task status %s", task_status.model_dump_json())
100103
await progress_manager.update(
101104
task_id=task_id,
102105
message=task_status.task_progress.message,
@@ -114,7 +117,7 @@ async def _wait_for_task_result() -> Any:
114117

115118
try:
116119
result = await asyncio.wait_for(_wait_for_task_result(), timeout=task_timeout)
117-
logger.debug("%s, %s", f"{task_id=}", f"{result=}")
120+
_logger.debug("%s, %s", f"{task_id=}", f"{result=}")
118121

119122
yield result
120123
except TimeoutError as e:
@@ -124,3 +127,13 @@ async def _wait_for_task_result() -> Any:
124127
timeout=task_timeout,
125128
exception=e,
126129
) from e
130+
except Exception as e:
131+
error = TaskExceptionError(task_id=task_id, exception=e, traceback="")
132+
_logger.warning(
133+
create_troubleshotting_log_message(
134+
user_error_msg=f"{task_id=} raised an exception",
135+
error=e,
136+
tip=f"Check the logs of the service responding to '{client.base_url}'",
137+
)
138+
)
139+
raise error from e

0 commit comments

Comments
 (0)