Skip to content

Commit a4bc47c

Browse files
committed
Merge branch 'master' into pr/giancarloromeo/8141
2 parents 2ec1124 + dd684b7 commit a4bc47c

File tree

103 files changed

+4198
-1522
lines changed

Some content is hidden

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

103 files changed

+4198
-1522
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
applyTo: '**'
3+
---
4+
Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.
5+
6+
## General Guidelines
7+
8+
1. **Test-Driven Development**: Write unit tests for all new functions and features. Use `pytest` for Python and appropriate testing frameworks for Node.js.
9+
2. **Environment Variables**: Use [Environment Variables Guide](../../docs/env-vars.md) for configuration. Avoid hardcoding sensitive information.
10+
3. **Documentation**: Prefer self-explanatory code; add documentation only if explicitly requested by the developer. Be concise.
11+
4. **Code Reviews**: Participate in code reviews and provide constructive feedback.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
applyTo: '**/*.js'
3+
---
4+
Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.
5+
6+
## 🛠️Coding Instructions for Node.js in This Repository
7+
8+
* Use ES6+ syntax and features.
9+
* Follow the `package.json` configuration for dependencies and scripts.
10+
* Use `eslint` for linting and `prettier` for code formatting.
11+
* Write modular and reusable code, adhering to the project's structure.

.github/copilot-instructions.md renamed to .github/instructions/python.instructions.md

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
1-
# GitHub Copilot Instructions
2-
3-
This document provides guidelines and best practices for using GitHub Copilot in the `osparc-simcore` repository and other Python and Node.js projects.
4-
5-
## General Guidelines
6-
7-
1. **Test-Driven Development**: Write unit tests for all new functions and features. Use `pytest` for Python and appropriate testing frameworks for Node.js.
8-
2. **Environment Variables**: Use [Environment Variables Guide](../docs/env-vars.md) for configuration. Avoid hardcoding sensitive information.
9-
3. **Documentation**: Prefer self-explanatory code; add documentation only if explicitly requested by the developer.
10-
111
---
2+
applyTo: '**/*.py'
3+
---
4+
Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.
125

136
## 🛠️Coding Instructions for Python in This Repository
147

@@ -25,8 +18,8 @@ Follow these rules **strictly** when generating Python code:
2518

2619
### 3. **Code Style & Formatting**
2720

28-
* Follow [Python Coding Conventions](../docs/coding-conventions.md) **strictly**.
29-
* Format code with `black`.
21+
* Follow [Python Coding Conventions](../../docs/coding-conventions.md) **strictly**.
22+
* Format code with `black` and `ruff`.
3023
* Lint code with `ruff` and `pylint`.
3124

3225
### 4. **Library Compatibility**
@@ -51,11 +44,6 @@ Ensure compatibility with the following library versions:
5144
* Prefer `json_dumps` / `json_loads` from `common_library.json_serialization` instead of the built-in `json.dumps` / `json.loads`.
5245
* When using Pydantic models, prefer methods like `model.model_dump_json()` for serialization.
5346

54-
---
55-
56-
## 🛠️Coding Instructions for Node.js in This Repository
57-
58-
* Use ES6+ syntax and features.
59-
* Follow the `package.json` configuration for dependencies and scripts.
60-
* Use `eslint` for linting and `prettier` for code formatting.
61-
* Write modular and reusable code, adhering to the project's structure.
47+
### 7. **Running tests**
48+
* Use `--keep-docker-up` flag when testing to keep docker containers up between sessions.
49+
* Always activate the python virtual environment before running pytest.

packages/celery-library/src/celery_library/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,11 @@ def decode_celery_transferrable_error(error: TransferrableCeleryError) -> Except
2626
return result
2727

2828

29+
class TaskSubmissionError(OsparcErrorMixin, Exception):
30+
msg_template = (
31+
"Unable to submit task {task_name} with id '{task_id}' and params {task_params}"
32+
)
33+
34+
2935
class TaskNotFoundError(OsparcErrorMixin, Exception):
3036
msg_template = "Task with id '{task_id}' was not found"

packages/celery-library/src/celery_library/task_manager.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from uuid import uuid4
55

66
from celery import Celery # type: ignore[import-untyped]
7+
from celery.exceptions import CeleryError # type: ignore[import-untyped]
78
from common_library.async_tools import make_async
89
from models_library.progress_bar import ProgressReport
910
from servicelib.celery.models import (
@@ -21,7 +22,7 @@
2122
from servicelib.logging_utils import log_context
2223
from settings_library.celery import CelerySettings
2324

24-
from .errors import TaskNotFoundError
25+
from .errors import TaskNotFoundError, TaskSubmissionError
2526

2627
_logger = logging.getLogger(__name__)
2728

@@ -50,21 +51,38 @@ async def submit_task(
5051
):
5152
task_uuid = uuid4()
5253
task_id = task_filter.create_task_id(task_uuid=task_uuid)
53-
self._celery_app.send_task(
54-
task_metadata.name,
55-
task_id=task_id,
56-
kwargs={"task_id": task_id} | task_params,
57-
queue=task_metadata.queue.value,
58-
)
5954

6055
expiry = (
6156
self._celery_settings.CELERY_EPHEMERAL_RESULT_EXPIRES
6257
if task_metadata.ephemeral
6358
else self._celery_settings.CELERY_RESULT_EXPIRES
6459
)
65-
await self._task_info_store.create_task(
66-
task_id, task_metadata, expiry=expiry
67-
)
60+
61+
try:
62+
await self._task_info_store.create_task(
63+
task_id, task_metadata, expiry=expiry
64+
)
65+
self._celery_app.send_task(
66+
task_metadata.name,
67+
task_id=task_id,
68+
kwargs={"task_id": task_id} | task_params,
69+
queue=task_metadata.queue.value,
70+
)
71+
except CeleryError as exc:
72+
try:
73+
await self._task_info_store.remove_task(task_id)
74+
except CeleryError:
75+
_logger.warning(
76+
"Unable to cleanup task '%s' during error handling",
77+
task_id,
78+
exc_info=True,
79+
)
80+
raise TaskSubmissionError(
81+
task_name=task_metadata.name,
82+
task_id=task_id,
83+
task_params=task_params,
84+
) from exc
85+
6886
return task_uuid
6987

7088
async def cancel_task(self, task_filter: TaskFilter, task_uuid: TaskUUID) -> None:

packages/common-library/src/common_library/async_tools.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,32 +89,37 @@ async def cancel_wait_task(
8989
TimeoutError: raised if cannot cancel the task.
9090
CancelledError: raised ONLY if owner is being cancelled.
9191
"""
92+
if task.done():
93+
# nothing to do here
94+
return
9295

93-
cancelling = task.cancel()
94-
if not cancelling:
95-
return # task was alredy cancelled
96-
97-
assert task.cancelling() # nosec
98-
assert not task.cancelled() # nosec
99-
96+
# mark for cancellation
97+
task.cancel("cancel_wait_task was called to cancel this task")
10098
try:
101-
99+
_logger.debug("Cancelling task %s", task.get_name())
102100
await asyncio.shield(
103101
# NOTE shield ensures that cancellation of the caller function won't stop you
104102
# from observing the cancellation/finalization of task.
105103
asyncio.wait_for(task, timeout=max_delay)
106104
)
107-
108-
except asyncio.CancelledError:
109-
if not task.cancelled():
110-
# task owner function is being cancelled -> propagate cancellation
111-
raise
112-
113-
# else: task cancellation is complete, we can safely ignore it
114-
_logger.debug(
115-
"Task %s cancellation is complete",
105+
except TimeoutError:
106+
_logger.exception(
107+
"Timeout while cancelling task %s after %s seconds",
116108
task.get_name(),
109+
max_delay,
117110
)
111+
raise
112+
except asyncio.CancelledError:
113+
current_task = asyncio.current_task()
114+
assert current_task is not None # nosec
115+
if current_task.cancelling() > 0:
116+
# owner function is being cancelled -> propagate cancellation
117+
raise
118+
finally:
119+
if not task.done():
120+
_logger.error("Failed to cancel %s", task.get_name())
121+
else:
122+
_logger.debug("Task %s cancelled", task.get_name())
118123

119124

120125
def delayed_start(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Update api-keys uniqueness constraint
2+
3+
Revision ID: 7e92447558e0
4+
Revises: 06eafd25d004
5+
Create Date: 2025-09-12 09:56:45.164921+00:00
6+
7+
"""
8+
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "7e92447558e0"
13+
down_revision = "06eafd25d004"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.drop_constraint("display_name_userid_uniqueness", "api_keys", type_="unique")
21+
op.create_unique_constraint(
22+
"display_name_userid_product_name_uniqueness",
23+
"api_keys",
24+
["display_name", "user_id", "product_name"],
25+
)
26+
# ### end Alembic commands ###
27+
28+
29+
def downgrade():
30+
# ### commands auto generated by Alembic - please adjust! ###
31+
op.drop_constraint(
32+
"display_name_userid_product_name_uniqueness", "api_keys", type_="unique"
33+
)
34+
op.create_unique_constraint(
35+
"display_name_userid_uniqueness", "api_keys", ["display_name", "user_id"]
36+
)
37+
# ### end Alembic commands ###

packages/postgres-database/src/simcore_postgres_database/models/api_keys.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@
7575
"If set to NULL then the key does not expire.",
7676
),
7777
sa.UniqueConstraint(
78-
"display_name", "user_id", name="display_name_userid_uniqueness"
78+
"display_name",
79+
"user_id",
80+
"product_name",
81+
name="display_name_userid_product_name_uniqueness",
7982
),
8083
)
8184

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# pylint: disable=protected-access
2+
3+
import pytest
4+
from fastapi import FastAPI
5+
from servicelib.long_running_tasks.errors import TaskNotFoundError
6+
from servicelib.long_running_tasks.manager import (
7+
LongRunningManager,
8+
)
9+
from servicelib.long_running_tasks.models import TaskContext
10+
from servicelib.long_running_tasks.task import TaskId
11+
from tenacity import (
12+
AsyncRetrying,
13+
retry_if_not_exception_type,
14+
stop_after_delay,
15+
wait_fixed,
16+
)
17+
18+
19+
def get_fastapi_long_running_manager(app: FastAPI) -> LongRunningManager:
20+
manager = app.state.long_running_manager
21+
assert isinstance(manager, LongRunningManager)
22+
return manager
23+
24+
25+
async def assert_task_is_no_longer_present(
26+
manager: LongRunningManager, task_id: TaskId, task_context: TaskContext
27+
) -> None:
28+
async for attempt in AsyncRetrying(
29+
reraise=True,
30+
wait=wait_fixed(0.1),
31+
stop=stop_after_delay(60),
32+
retry=retry_if_not_exception_type(TaskNotFoundError),
33+
):
34+
with attempt: # noqa: SIM117
35+
with pytest.raises(TaskNotFoundError):
36+
# use internals to detirmine when it's no longer here
37+
await manager._tasks_manager._get_tracked_task( # noqa: SLF001
38+
task_id, task_context
39+
)

packages/service-library/setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ markers =
2121
testit: "marks test to run during development"
2222
performance_test: "performance test"
2323
no_cleanup_check_rabbitmq_server_has_no_errors: "no check in rabbitmq logs"
24+
heavy_load: "marks test as heavy load"
2425

2526
[mypy]
2627
plugins =

0 commit comments

Comments
 (0)