Skip to content

Commit 60e0357

Browse files
authored
Merge branch 'master' into 2025/fix/director-v0-url-type
2 parents f613647 + 8918afb commit 60e0357

File tree

167 files changed

+6051
-2797
lines changed

Some content is hidden

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

167 files changed

+6051
-2797
lines changed

.env-devel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ CATALOG_SERVICES_DEFAULT_RESOURCES='{"CPU": {"limit": 0.1, "reservation": 0.1},
4949
CATALOG_SERVICES_DEFAULT_SPECIFICATIONS='{}'
5050
CATALOG_TRACING=null
5151

52+
CELERY_RESULT_EXPIRES=P7D
53+
5254
CLUSTERS_KEEPER_COMPUTATIONAL_BACKEND_DEFAULT_CLUSTER_AUTH='{"type":"tls","tls_ca_file":"/home/scu/.dask/dask-crt.pem","tls_client_cert":"/home/scu/.dask/dask-crt.pem","tls_client_key":"/home/scu/.dask/dask-key.pem"}'
5355
CLUSTERS_KEEPER_COMPUTATIONAL_BACKEND_DOCKER_IMAGE_TAG=master-github-latest
5456
CLUSTERS_KEEPER_DASK_NTHREADS=0

.github/workflows/ci-testing-pull-request.yml

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ on:
1010
branches:
1111
- "master"
1212

13+
workflow_dispatch:
14+
inputs:
15+
target_repo:
16+
description: full repository name (e.g. 'ITISFoundation/osparc-simcore')
17+
required: true
18+
default: "ITISFoundation/osparc-simcore"
19+
type: environment
20+
target_branch:
21+
description: Check backwards compatibility against target_branch in target_repo
22+
required: true
23+
default: "master"
24+
type: environment
25+
26+
1327
concurrency:
1428
group: ${{ github.workflow }}-${{ github.ref }}
1529
cancel-in-progress: true
@@ -39,7 +53,7 @@ jobs:
3953
- name: Check openapi specs are up to date
4054
run: |
4155
if ! ./ci/github/helpers/openapi-specs-diff.bash diff \
42-
https://raw.githubusercontent.com/${{ github.event.pull_request.head.repo.full_name }}/${{ github.event.pull_request.head.sha }} \
56+
"https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$GITHUB_SHA" \
4357
.; then \
4458
echo "::error:: OAS are not up to date. Run 'make openapi-specs' to update them"; exit 1; \
4559
fi
@@ -56,10 +70,19 @@ jobs:
5670
python-version: "3.11"
5771
- name: checkout
5872
uses: actions/checkout@v4
73+
- name: Set environment variables based on event type
74+
run: |
75+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
76+
echo "REPO=${{ inputs.target_repo }}" >> $GITHUB_ENV
77+
echo "BRANCH=${{ inputs.target_branch }}" >> $GITHUB_ENV
78+
else
79+
echo "REPO=${{ github.event.pull_request.base.repo.full_name }}" >> $GITHUB_ENV
80+
echo "BRANCH=${{ github.base_ref }}" >> $GITHUB_ENV
81+
fi
5982
- name: check api-server backwards compatibility
6083
run: |
6184
./scripts/openapi-diff.bash breaking --fail-on ERR\
62-
https://raw.githubusercontent.com/${{ github.event.pull_request.base.repo.full_name }}/refs/heads/${{ github.base_ref }}/services/api-server/openapi.json \
85+
"https://raw.githubusercontent.com/$REPO/refs/heads/$BRANCH/services/api-server/openapi.json" \
6386
/specs/services/api-server/openapi.json
6487
6588
all-oas-breaking:
@@ -75,8 +98,17 @@ jobs:
7598
python-version: "3.11"
7699
- name: checkout
77100
uses: actions/checkout@v4
101+
- name: Set environment variables based on event type
102+
run: |
103+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
104+
echo "REPO=${{ inputs.target_repo }}" >> $GITHUB_ENV
105+
echo "BRANCH=${{ inputs.target_branch }}" >> $GITHUB_ENV
106+
else
107+
echo "REPO=${{ github.event.pull_request.base.repo.full_name }}" >> $GITHUB_ENV
108+
echo "BRANCH=${{ github.base_ref }}" >> $GITHUB_ENV
109+
fi
78110
- name: Check openapi-specs backwards compatibility
79111
run: |
80112
./ci/github/helpers/openapi-specs-diff.bash breaking \
81-
https://raw.githubusercontent.com/${{ github.event.pull_request.base.repo.full_name }}/refs/heads/${{ github.base_ref }} \
113+
"https://raw.githubusercontent.com/$REPO/refs/heads/$BRANCH" \
82114
.

api/specs/web-server/_auth_api_keys.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from models_library.rest_error import EnvelopedError
1111
from simcore_service_webserver._meta import API_VTAG
1212
from simcore_service_webserver.api_keys._controller_rest import ApiKeysPathParams
13-
from simcore_service_webserver.api_keys._exceptions_handlers import _TO_HTTP_ERROR_MAP
13+
from simcore_service_webserver.api_keys._controller_rest_exceptions import (
14+
_TO_HTTP_ERROR_MAP,
15+
)
1416

1517
router = APIRouter(
1618
prefix=f"/{API_VTAG}",

api/specs/web-server/_catalog.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from fastapi import APIRouter, Depends
44
from models_library.api_schemas_api_server.pricing_plans import ServicePricingPlanGet
55
from models_library.api_schemas_webserver.catalog import (
6+
CatalogLatestServiceGet,
67
CatalogServiceGet,
7-
CatalogServiceListItem,
88
CatalogServiceUpdate,
99
ServiceInputGet,
1010
ServiceInputKey,
@@ -15,13 +15,13 @@
1515
from models_library.generics import Envelope
1616
from models_library.rest_pagination import Page
1717
from simcore_service_webserver._meta import API_VTAG
18-
from simcore_service_webserver.catalog._rest_controller import (
18+
from simcore_service_webserver.catalog._controller_rest_schemas import (
19+
FromServiceOutputQueryParams,
1920
ListServiceParams,
21+
ServiceInputsPathParams,
22+
ServiceOutputsPathParams,
2023
ServicePathParams,
21-
_FromServiceOutputParams,
22-
_ServiceInputsPathParams,
23-
_ServiceOutputsPathParams,
24-
_ToServiceInputsParams,
24+
ToServiceInputsQueryParams,
2525
)
2626

2727
router = APIRouter(
@@ -34,10 +34,9 @@
3434

3535
@router.get(
3636
"/catalog/services/-/latest",
37-
response_model=Page[CatalogServiceListItem],
37+
response_model=Page[CatalogLatestServiceGet],
3838
)
39-
def list_services_latest(_query: Annotated[ListServiceParams, Depends()]):
40-
pass
39+
def list_services_latest(_query: Annotated[ListServiceParams, Depends()]): ...
4140

4241

4342
@router.get(
@@ -71,7 +70,7 @@ def list_service_inputs(
7170
response_model=Envelope[ServiceInputGet],
7271
)
7372
def get_service_input(
74-
_path: Annotated[_ServiceInputsPathParams, Depends()],
73+
_path: Annotated[ServiceInputsPathParams, Depends()],
7574
): ...
7675

7776

@@ -81,7 +80,7 @@ def get_service_input(
8180
)
8281
def get_compatible_inputs_given_source_output(
8382
_path: Annotated[ServicePathParams, Depends()],
84-
_query: Annotated[_FromServiceOutputParams, Depends()],
83+
_query: Annotated[FromServiceOutputQueryParams, Depends()],
8584
): ...
8685

8786

@@ -99,7 +98,7 @@ def list_service_outputs(
9998
response_model=Envelope[list[ServiceOutputGet]],
10099
)
101100
def get_service_output(
102-
_path: Annotated[_ServiceOutputsPathParams, Depends()],
101+
_path: Annotated[ServiceOutputsPathParams, Depends()],
103102
): ...
104103

105104

@@ -109,7 +108,7 @@ def get_service_output(
109108
)
110109
def get_compatible_outputs_given_target_input(
111110
_path: Annotated[ServicePathParams, Depends()],
112-
_query: Annotated[_ToServiceInputsParams, Depends()],
111+
_query: Annotated[ToServiceInputsQueryParams, Depends()],
113112
): ...
114113

115114

api/specs/web-server/_catalog_tags.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from models_library.api_schemas_webserver.catalog import CatalogServiceGet
1111
from models_library.generics import Envelope
1212
from simcore_service_webserver._meta import API_VTAG
13-
from simcore_service_webserver.catalog._rest_tags_controller import (
13+
from simcore_service_webserver.catalog._controller_rest_schemas import (
1414
ServicePathParams,
1515
ServiceTagPathParams,
1616
)

api/specs/web-server/_storage.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,15 +210,15 @@ async def export_data(data_export: DataExportPost, location_id: LocationID):
210210
response_model=Envelope[StorageAsyncJobStatus],
211211
name="get_async_job_status",
212212
)
213-
async def get_async_job_status(storage_async_job_get: StorageAsyncJobGet, job_id: UUID):
213+
async def get_async_job_status(job_id: UUID):
214214
"""Get async job status"""
215215

216216

217217
@router.post(
218218
"/storage/async-jobs/{job_id}:abort",
219219
name="abort_async_job",
220220
)
221-
async def abort_async_job(storage_async_job_get: StorageAsyncJobGet, job_id: UUID):
221+
async def abort_async_job(job_id: UUID):
222222
"""aborts execution of an async job"""
223223

224224

@@ -227,7 +227,7 @@ async def abort_async_job(storage_async_job_get: StorageAsyncJobGet, job_id: UUI
227227
response_model=Envelope[StorageAsyncJobResult],
228228
name="get_async_job_result",
229229
)
230-
async def get_async_job_result(storage_async_job_get: StorageAsyncJobGet, job_id: UUID):
230+
async def get_async_job_result(job_id: UUID):
231231
"""Get the result of the async job"""
232232

233233

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import asyncio
2+
import functools
3+
from collections.abc import Awaitable, Callable
4+
from concurrent.futures import Executor
5+
from typing import ParamSpec, TypeVar
6+
7+
R = TypeVar("R")
8+
P = ParamSpec("P")
9+
10+
11+
def make_async(
12+
executor: Executor | None = None,
13+
) -> Callable[[Callable[P, R]], Callable[P, Awaitable[R]]]:
14+
def decorator(func: Callable[P, R]) -> Callable[P, Awaitable[R]]:
15+
@functools.wraps(func)
16+
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
17+
loop = asyncio.get_running_loop()
18+
return await loop.run_in_executor(
19+
executor, functools.partial(func, *args, **kwargs)
20+
)
21+
22+
return wrapper
23+
24+
return decorator
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import asyncio
2+
from concurrent.futures import ThreadPoolExecutor
3+
4+
import pytest
5+
from common_library.async_tools import make_async
6+
7+
8+
@make_async()
9+
def sync_function(x: int, y: int) -> int:
10+
return x + y
11+
12+
13+
@make_async()
14+
def sync_function_with_exception() -> None:
15+
raise ValueError("This is an error!")
16+
17+
18+
@pytest.mark.asyncio
19+
async def test_make_async_returns_coroutine():
20+
result = sync_function(2, 3)
21+
assert asyncio.iscoroutine(result), "Function should return a coroutine"
22+
23+
24+
@pytest.mark.asyncio
25+
async def test_make_async_execution():
26+
result = await sync_function(2, 3)
27+
assert result == 5, "Function should return 5"
28+
29+
30+
@pytest.mark.asyncio
31+
async def test_make_async_exception():
32+
with pytest.raises(ValueError, match="This is an error!"):
33+
await sync_function_with_exception()
34+
35+
36+
@pytest.mark.asyncio
37+
async def test_make_async_with_executor():
38+
executor = ThreadPoolExecutor()
39+
40+
@make_async(executor)
41+
def heavy_computation(x: int) -> int:
42+
return x * x
43+
44+
result = await heavy_computation(4)
45+
assert result == 16, "Function should return 16"

packages/models-library/src/models_library/api_schemas_catalog/services.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,15 @@ class _BaseServiceGetV2(CatalogOutputSchema):
202202

203203
access_rights: dict[GroupID, ServiceGroupAccessRightsV2] | None
204204

205-
classifiers: list[str] | None = []
206-
quality: dict[str, Any] = {}
205+
classifiers: Annotated[
206+
list[str] | None,
207+
Field(default_factory=list),
208+
] = DEFAULT_FACTORY
209+
210+
quality: Annotated[
211+
dict[str, Any],
212+
Field(default_factory=dict),
213+
] = DEFAULT_FACTORY
207214

208215
model_config = ConfigDict(
209216
extra="forbid",
@@ -212,6 +219,34 @@ class _BaseServiceGetV2(CatalogOutputSchema):
212219
)
213220

214221

222+
class LatestServiceGet(_BaseServiceGetV2):
223+
release: Annotated[
224+
ServiceRelease,
225+
Field(description="release information of current (latest) service"),
226+
]
227+
228+
@staticmethod
229+
def _update_json_schema_extra(schema: JsonDict) -> None:
230+
schema.update(
231+
{
232+
"examples": [
233+
{
234+
**_EXAMPLE_SLEEPER, # v2.2.1 (latest)
235+
"release": {
236+
"version": _EXAMPLE_SLEEPER["version"],
237+
"version_display": "Summer Release",
238+
"released": "2025-07-20T15:00:00",
239+
},
240+
}
241+
]
242+
}
243+
)
244+
245+
model_config = ConfigDict(
246+
json_schema_extra=_update_json_schema_extra,
247+
)
248+
249+
215250
class ServiceGetV2(_BaseServiceGetV2):
216251
# Model used in catalog's rpc and rest interfaces
217252
history: Annotated[
@@ -235,7 +270,7 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
235270
{
236271
"version": _EXAMPLE_SLEEPER["version"],
237272
"version_display": "Summer Release",
238-
"released": "2024-07-20T15:00:00",
273+
"released": "2024-07-21T15:00:00",
239274
},
240275
{
241276
"version": "2.0.0",
@@ -263,7 +298,7 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
263298
},
264299
{
265300
"version": "0.9.0",
266-
"retired": "2024-07-20T15:00:00",
301+
"retired": "2024-07-20T16:00:00",
267302
},
268303
{"version": "0.8.0"},
269304
{"version": "0.1.0"},
@@ -288,21 +323,9 @@ def _update_json_schema_extra(schema: JsonDict) -> None:
288323
)
289324

290325

291-
class ServiceListItem(_BaseServiceGetV2):
292-
history: Annotated[
293-
list[ServiceRelease],
294-
Field(
295-
default_factory=list,
296-
deprecated=True,
297-
description="History will be replaced by current 'release' instead",
298-
json_schema_extra={"default": []},
299-
),
300-
] = DEFAULT_FACTORY
301-
302-
303326
PageRpcServicesGetV2: TypeAlias = PageRpc[
304327
# WARNING: keep this definition in models_library and not in the RPC interface
305-
ServiceListItem
328+
LatestServiceGet
306329
]
307330

308331
ServiceResourcesGet: TypeAlias = ServiceResourcesDict

packages/models-library/src/models_library/api_schemas_rpc_async_jobs/async_jobs.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
from datetime import datetime
21
from typing import Any, TypeAlias
32
from uuid import UUID
43

54
from models_library.users import UserID
6-
from pydantic import BaseModel, model_validator
7-
from typing_extensions import Self
5+
from pydantic import BaseModel
86

97
from ..progress_bar import ProgressReport
108

@@ -15,18 +13,6 @@ class AsyncJobStatus(BaseModel):
1513
job_id: AsyncJobId
1614
progress: ProgressReport
1715
done: bool
18-
started: datetime
19-
stopped: datetime | None
20-
21-
@model_validator(mode="after")
22-
def _check_consistency(self) -> Self:
23-
is_done = self.done
24-
is_stopped = self.stopped is not None
25-
26-
if is_done != is_stopped:
27-
msg = f"Inconsistent data: {self.done=}, {self.stopped=}"
28-
raise ValueError(msg)
29-
return self
3016

3117

3218
class AsyncJobResult(BaseModel):

0 commit comments

Comments
 (0)