Skip to content

Commit ef422ad

Browse files
authored
Merge branch 'master' into is7395/test-settings
2 parents 31b5d61 + 6903354 commit ef422ad

File tree

47 files changed

+1063
-398
lines changed

Some content is hidden

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

47 files changed

+1063
-398
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/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
)

services/api-server/src/simcore_service_api_server/api/routes/studies.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ async def clone_study(
111111
description=description,
112112
)
113113
await webserver_api.patch_project(
114-
project_id=study_id, patch_params=patch_params
114+
project_id=project.uuid, patch_params=patch_params
115115
)
116+
project = await webserver_api.get_project(project_id=project.uuid)
116117
return _create_study_from_project(project)
117118

118119

services/api-server/tests/unit/api_studies/test_api_routes_studies.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# pylint: disable=redefined-outer-name
22
# pylint: disable=unused-argument
33
# pylint: disable=unused-variable
4+
# pylint: disable=too-many-arguments
5+
# pylint: disable=too-many-statements
46

57

68
import json
@@ -13,6 +15,8 @@
1315
import pytest
1416
from faker import Faker
1517
from fastapi import status
18+
from models_library.api_schemas_webserver.projects import ProjectGet
19+
from models_library.generics import Envelope
1620
from pydantic import TypeAdapter
1721
from pytest_mock import MockType
1822
from pytest_simcore.helpers.httpx_calls_capture_models import HttpApiCallCaptureModel
@@ -224,12 +228,8 @@ async def test_clone_study_with_title(
224228
study_id: StudyID,
225229
mocked_webserver_rest_api_base: MockRouter,
226230
patch_webserver_long_running_project_tasks: Callable[[MockRouter], MockRouter],
227-
mock_webserver_patch_project: Callable[
228-
[
229-
MockRouter,
230-
],
231-
MockRouter,
232-
],
231+
mock_webserver_patch_project: Callable[[MockRouter], MockRouter],
232+
mock_webserver_get_project: Callable[[MockRouter], MockRouter],
233233
hidden: bool | None,
234234
title: str | None,
235235
description: str | None,
@@ -238,11 +238,14 @@ async def test_clone_study_with_title(
238238
# Mocks /projects
239239
patch_webserver_long_running_project_tasks(mocked_webserver_rest_api_base)
240240
mock_webserver_patch_project(mocked_webserver_rest_api_base)
241+
mock_webserver_get_project(mocked_webserver_rest_api_base)
241242

242243
create_callback = mocked_webserver_rest_api_base["create_projects"].side_effect
243244
assert create_callback is not None
244245
patch_callback = mocked_webserver_rest_api_base["project_patch"].side_effect
245246
assert patch_callback is not None
247+
get_callback = mocked_webserver_rest_api_base["project_get"].side_effect
248+
assert get_callback is not None
246249

247250
def clone_project_side_effect(request: httpx.Request):
248251
if hidden is not None:
@@ -260,12 +263,28 @@ def patch_project_side_effect(request: httpx.Request, *args, **kwargs):
260263
assert _description is not None and _description in description
261264
return patch_callback(request, *args, **kwargs)
262265

266+
def get_project_side_effect(request: httpx.Request, *args, **kwargs):
267+
# this is needed to return the patched project
268+
_project_id = kwargs.get("project_id")
269+
assert _project_id is not None
270+
result = Envelope[ProjectGet].model_validate(
271+
{"data": ProjectGet.model_json_schema()["examples"][0]}
272+
)
273+
assert result.data is not None
274+
if title is not None:
275+
result.data.name = title
276+
if description is not None:
277+
result.data.description = description
278+
result.data.uuid = UUID(_project_id)
279+
return httpx.Response(status.HTTP_200_OK, content=result.model_dump_json())
280+
263281
mocked_webserver_rest_api_base["create_projects"].side_effect = (
264282
clone_project_side_effect
265283
)
266284
mocked_webserver_rest_api_base["project_patch"].side_effect = (
267285
patch_project_side_effect
268286
)
287+
mocked_webserver_rest_api_base["project_get"].side_effect = get_project_side_effect
269288

270289
query = dict()
271290
if hidden is not None:
@@ -286,8 +305,14 @@ def patch_project_side_effect(request: httpx.Request, *args, **kwargs):
286305
assert mocked_webserver_rest_api_base["create_projects"].called
287306
if title or description:
288307
assert mocked_webserver_rest_api_base["project_patch"].called
308+
assert mocked_webserver_rest_api_base["project_get"].called
289309

290310
assert resp.status_code == expected_status_code
311+
study = Study.model_validate(resp.json())
312+
if title is not None:
313+
assert study.title == title
314+
if description is not None:
315+
assert study.description == description
291316

292317

293318
async def test_clone_study_not_found(

services/api-server/tests/unit/conftest.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ def _mock(webserver_mock_router: MockRouter) -> MockRouter:
717717

718718
@pytest.fixture
719719
def mock_webserver_patch_project(
720-
app: FastAPI, faker: Faker, services_mocks_enabled: bool
720+
app: FastAPI, services_mocks_enabled: bool
721721
) -> Callable[[MockRouter], MockRouter]:
722722
settings: ApplicationSettings = app.state.settings
723723
assert settings.API_SERVER_WEBSERVER is not None
@@ -736,6 +736,30 @@ def _patch_project(request: httpx.Request, *args, **kwargs):
736736
return _mock
737737

738738

739+
@pytest.fixture
740+
def mock_webserver_get_project(
741+
app: FastAPI, services_mocks_enabled: bool
742+
) -> Callable[[MockRouter], MockRouter]:
743+
settings: ApplicationSettings = app.state.settings
744+
assert settings.API_SERVER_WEBSERVER is not None
745+
746+
def _mock(webserver_mock_router: MockRouter) -> MockRouter:
747+
def _get_project(request: httpx.Request, *args, **kwargs):
748+
result = Envelope[ProjectGet].model_validate(
749+
{"data": ProjectGet.model_json_schema()["examples"][0]}
750+
)
751+
return httpx.Response(status.HTTP_200_OK, json=result.model_dump())
752+
753+
if services_mocks_enabled:
754+
webserver_mock_router.get(
755+
path__regex=r"/projects/(?P<project_id>[\w-]+)$",
756+
name="project_get",
757+
).mock(side_effect=_get_project)
758+
return webserver_mock_router
759+
760+
return _mock
761+
762+
739763
@pytest.fixture
740764
def openapi_dev_specs(project_slug_dir: Path) -> dict[str, Any]:
741765
openapi_file = (project_slug_dir / "openapi-dev.json").resolve()

services/director-v2/src/simcore_service_director_v2/modules/comp_scheduler/_scheduler_base.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -590,9 +590,7 @@ async def _process_executing_tasks(
590590
"""process executing tasks from the 3rd party backend"""
591591

592592
@abstractmethod
593-
async def _release_resources(
594-
self, user_id: UserID, project_id: ProjectID, comp_run: CompRunsAtDB
595-
) -> None:
593+
async def _release_resources(self, comp_run: CompRunsAtDB) -> None:
596594
"""release resources used by the scheduler for a given user and project"""
597595

598596
async def apply(
@@ -660,7 +658,7 @@ async def apply(
660658

661659
# 7. Are we done scheduling that pipeline?
662660
if not dag.nodes() or pipeline_result in COMPLETED_STATES:
663-
await self._release_resources(user_id, project_id, comp_run)
661+
await self._release_resources(comp_run)
664662
# there is nothing left, the run is completed, we're done here
665663
_logger.info(
666664
"pipeline %s scheduling completed with result %s",

0 commit comments

Comments
 (0)