Skip to content

Commit a5ebd01

Browse files
Merge remote-tracking branch 'upstream/master' into extract-celery-code
2 parents abc94de + 3c20e92 commit a5ebd01

File tree

8 files changed

+117
-26
lines changed

8 files changed

+117
-26
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,84 +2,100 @@
22

33

44
class FunctionBaseError(OsparcErrorMixin, Exception):
5-
pass
5+
status_code: int
66

77

88
class FunctionJobReadAccessDeniedError(FunctionBaseError):
99
msg_template: str = (
1010
"Function job {function_job_id} read access denied for user {user_id}"
1111
)
12+
status_code: int = 403 # Forbidden
1213

1314

1415
class FunctionIDNotFoundError(FunctionBaseError):
1516
msg_template: str = "Function {function_id} not found"
17+
status_code: int = 404 # Not Found
1618

1719

1820
class FunctionJobIDNotFoundError(FunctionBaseError):
1921
msg_template: str = "Function job {function_job_id} not found"
22+
status_code: int = 404 # Not Found
2023

2124

2225
class FunctionInputsValidationError(FunctionBaseError):
2326
msg_template: str = "Function inputs validation failed: {error}"
27+
status_code: int = 422 # Unprocessable Entity
2428

2529

2630
class FunctionReadAccessDeniedError(FunctionBaseError):
2731
msg_template: str = "Function {function_id} read access denied for user {user_id}"
32+
status_code: int = 403 # Forbidden
2833

2934

3035
class FunctionJobCollectionIDNotFoundError(FunctionBaseError):
3136
msg_template: str = "Function job collection {function_job_collection_id} not found"
37+
status_code: int = 404 # Not Found
3238

3339

3440
class UnsupportedFunctionClassError(FunctionBaseError):
3541
msg_template: str = "Function class {function_class} is not supported"
42+
status_code: int = 400 # Bad Request
3643

3744

3845
class UnsupportedFunctionJobClassError(FunctionBaseError):
3946
msg_template: str = "Function job class {function_job_class} is not supported"
47+
status_code: int = 400 # Bad Request
4048

4149

4250
class UnsupportedFunctionFunctionJobClassCombinationError(FunctionBaseError):
4351
msg_template: str = (
4452
"Function class {function_class} and function job class {function_job_class} combination is not supported"
4553
)
54+
status_code: int = 400 # Bad Request
4655

4756

4857
class FunctionJobCollectionReadAccessDeniedError(FunctionBaseError):
4958
msg_template: str = (
5059
"Function job collection {function_job_collection_id} read access denied for user {user_id}"
5160
)
61+
status_code: int = 403 # Forbidden
5262

5363

5464
class FunctionWriteAccessDeniedError(FunctionBaseError):
5565
msg_template: str = "Function {function_id} write access denied for user {user_id}"
66+
status_code: int = 403 # Forbidden
5667

5768

5869
class FunctionJobWriteAccessDeniedError(FunctionBaseError):
5970
msg_template: str = (
6071
"Function job {function_job_id} write access denied for user {user_id}"
6172
)
73+
status_code: int = 403 # Forbidden
6274

6375

6476
class FunctionJobCollectionWriteAccessDeniedError(FunctionBaseError):
6577
msg_template: str = (
6678
"Function job collection {function_job_collection_id} write access denied for user {user_id}"
6779
)
80+
status_code: int = 403 # Forbidden
6881

6982

7083
class FunctionExecuteAccessDeniedError(FunctionBaseError):
7184
msg_template: str = (
7285
"Function {function_id} execute access denied for user {user_id}"
7386
)
87+
status_code: int = 403 # Forbidden
7488

7589

7690
class FunctionJobExecuteAccessDeniedError(FunctionBaseError):
7791
msg_template: str = (
7892
"Function job {function_job_id} execute access denied for user {user_id}"
7993
)
94+
status_code: int = 403 # Forbidden
8095

8196

8297
class FunctionJobCollectionExecuteAccessDeniedError(FunctionBaseError):
8398
msg_template: str = (
8499
"Function job collection {function_job_collection_id} execute access denied for user {user_id}"
85100
)
101+
status_code: int = 403 # Forbidden

packages/simcore-sdk/requirements/_base.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
aiocache
1414
aiofiles
1515
aiohttp
16+
httpx
1617
packaging
1718
pint
18-
sqlalchemy[asyncio]
1919
pydantic[email]
20+
sqlalchemy[asyncio]
2021
tenacity
2122
tqdm

packages/simcore-sdk/requirements/_base.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ anyio==4.8.0
4444
# via
4545
# fast-depends
4646
# faststream
47+
# httpx
4748
arrow==1.3.0
4849
# via
4950
# -r requirements/../../../packages/models-library/requirements/_base.in
@@ -72,6 +73,8 @@ certifi==2025.1.31
7273
# -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
7374
# -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
7475
# -c requirements/../../../requirements/constraints.txt
76+
# httpcore
77+
# httpx
7578
# requests
7679
charset-normalizer==3.4.1
7780
# via requests
@@ -109,10 +112,32 @@ greenlet==3.1.1
109112
# via sqlalchemy
110113
grpcio==1.70.0
111114
# via opentelemetry-exporter-otlp-proto-grpc
115+
h11==0.16.0
116+
# via httpcore
117+
httpcore==1.0.9
118+
# via httpx
119+
httpx==0.28.1
120+
# via
121+
# -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
122+
# -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
123+
# -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
124+
# -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
125+
# -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt
126+
# -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
127+
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
128+
# -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt
129+
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
130+
# -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
131+
# -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt
132+
# -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt
133+
# -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt
134+
# -c requirements/../../../requirements/constraints.txt
135+
# -r requirements/_base.in
112136
idna==3.10
113137
# via
114138
# anyio
115139
# email-validator
140+
# httpx
116141
# requests
117142
# yarl
118143
importlib-metadata==8.5.0

packages/simcore-sdk/src/simcore_sdk/node_ports_common/file_io_utils.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from typing import IO, Any, Final, Protocol, runtime_checkable
88

99
import aiofiles
10+
import httpx
1011
from aiohttp import (
1112
ClientConnectionError,
1213
ClientError,
13-
ClientPayloadError,
1414
ClientResponse,
1515
ClientResponseError,
1616
ClientSession,
@@ -39,6 +39,7 @@
3939
from tqdm.contrib.logging import tqdm_logging_redirect
4040
from yarl import URL
4141

42+
from ..config.http_clients import client_request_settings
4243
from . import exceptions
4344
from .constants import CHUNK_SIZE
4445

@@ -148,13 +149,13 @@ async def __call__(self, log: str) -> None: ...
148149

149150
async def _file_chunk_writer(
150151
file: Path,
151-
response: ClientResponse,
152+
response: httpx.Response,
152153
pbar: tqdm,
153154
io_log_redirect_cb: LogRedirectCB | None,
154155
progress_bar: ProgressBarData,
155156
):
156157
async with aiofiles.open(file, "wb") as file_pointer:
157-
while chunk := await response.content.read(CHUNK_SIZE):
158+
async for chunk in response.aiter_bytes(CHUNK_SIZE):
158159
await file_pointer.write(chunk)
159160
if io_log_redirect_cb and pbar.update(len(chunk)):
160161
with log_catch(_logger, reraise=False):
@@ -172,7 +173,6 @@ async def _file_chunk_writer(
172173

173174

174175
async def download_link_to_file(
175-
session: ClientSession,
176176
url: URL,
177177
file_path: Path,
178178
*,
@@ -185,16 +185,25 @@ async def download_link_to_file(
185185
reraise=True,
186186
wait=wait_exponential(min=1, max=10),
187187
stop=stop_after_attempt(num_retries),
188-
retry=retry_if_exception_type(ClientConnectionError),
188+
retry=retry_if_exception_type(httpx.TransportError),
189189
before_sleep=before_sleep_log(_logger, logging.WARNING, exc_info=True),
190190
after=after_log(_logger, log_level=logging.ERROR),
191191
):
192192
with attempt:
193193
async with AsyncExitStack() as stack:
194-
response = await stack.enter_async_context(session.get(url))
195-
if response.status == status.HTTP_404_NOT_FOUND:
194+
client = await stack.enter_async_context(
195+
httpx.AsyncClient(
196+
timeout=httpx.Timeout(
197+
client_request_settings.HTTP_CLIENT_REQUEST_TOTAL_TIMEOUT
198+
)
199+
)
200+
)
201+
response = await stack.enter_async_context(
202+
client.stream("GET", f"{url}")
203+
)
204+
if response.status_code == status.HTTP_404_NOT_FOUND:
196205
raise exceptions.InvalidDownloadLinkError(url)
197-
if response.status > _VALID_HTTP_STATUS_CODES:
206+
if response.status_code > _VALID_HTTP_STATUS_CODES:
198207
raise exceptions.TransferError(url)
199208
file_path.parent.mkdir(parents=True, exist_ok=True)
200209
# SEE https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length
@@ -231,7 +240,7 @@ async def download_link_to_file(
231240
sub_progress,
232241
)
233242
_logger.debug("Download complete")
234-
except ClientPayloadError as exc:
243+
except httpx.HTTPError as exc:
235244
raise exceptions.TransferError(url) from exc
236245

237246

packages/simcore-sdk/src/simcore_sdk/node_ports_common/filemanager.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ async def download_path_from_s3(
217217
return await download_file_from_link(
218218
download_link,
219219
local_path,
220-
client_session=session,
221220
io_log_redirect_cb=io_log_redirect_cb,
222221
progress_bar=progress_bar,
223222
)
@@ -229,7 +228,6 @@ async def download_file_from_link(
229228
*,
230229
io_log_redirect_cb: LogRedirectCB | None,
231230
file_name: str | None = None,
232-
client_session: ClientSession | None = None,
233231
progress_bar: ProgressBarData,
234232
) -> Path:
235233
# a download link looks something like:
@@ -242,15 +240,14 @@ async def download_file_from_link(
242240

243241
if io_log_redirect_cb:
244242
await io_log_redirect_cb(f"downloading {local_file_path}, please wait...")
245-
async with ClientSessionContextManager(client_session) as session:
246-
await download_link_to_file(
247-
session,
248-
download_link,
249-
local_file_path,
250-
num_retries=NodePortsSettings.create_from_envs().NODE_PORTS_IO_NUM_RETRY_ATTEMPTS,
251-
io_log_redirect_cb=io_log_redirect_cb,
252-
progress_bar=progress_bar,
253-
)
243+
244+
await download_link_to_file(
245+
download_link,
246+
local_file_path,
247+
num_retries=NodePortsSettings.create_from_envs().NODE_PORTS_IO_NUM_RETRY_ATTEMPTS,
248+
io_log_redirect_cb=io_log_redirect_cb,
249+
progress_bar=progress_bar,
250+
)
254251
if io_log_redirect_cb:
255252
await io_log_redirect_cb(f"download of {local_file_path} complete.")
256253
return local_file_path

services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from fastapi import FastAPI
22
from fastapi.exceptions import RequestValidationError
33
from httpx import HTTPError as HttpxException
4+
from models_library.functions_errors import FunctionBaseError
45
from starlette import status
56
from starlette.exceptions import HTTPException
67

@@ -9,6 +10,7 @@
910
from ..custom_errors import CustomBaseError
1011
from ..log_streaming_errors import LogStreamingBaseError
1112
from ._custom_errors import custom_error_handler
13+
from ._handler_function_errors import function_error_handler
1214
from ._handlers_backend_errors import backend_error_handler
1315
from ._handlers_factory import make_handler_for_exception
1416
from ._http_exceptions import http_exception_handler
@@ -24,6 +26,7 @@ def setup(app: FastAPI, *, is_debug: bool = False):
2426
app.add_exception_handler(LogStreamingBaseError, log_handling_error_handler)
2527
app.add_exception_handler(CustomBaseError, custom_error_handler)
2628
app.add_exception_handler(BaseBackEndError, backend_error_handler)
29+
app.add_exception_handler(FunctionBaseError, function_error_handler)
2730

2831
# SEE https://docs.python.org/3/library/exceptions.html#exception-hierarchy
2932
app.add_exception_handler(
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from fastapi import Request
2+
from models_library.functions_errors import FunctionBaseError
3+
4+
from ._utils import create_error_json_response
5+
6+
7+
async def function_error_handler(request: Request, exc: Exception):
8+
assert request # nosec
9+
assert isinstance(exc, FunctionBaseError)
10+
11+
return create_error_json_response(f"{exc}", status_code=exc.status_code)

services/api-server/tests/unit/api_functions/test_api_routers_functions.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from uuid import uuid4
88

99
import httpx
10-
import pytest
1110
from httpx import AsyncClient
1211
from models_library.api_schemas_webserver.functions import (
1312
FunctionJobCollection,
@@ -17,7 +16,10 @@
1716
RegisteredProjectFunction,
1817
RegisteredProjectFunctionJob,
1918
)
20-
from models_library.functions_errors import FunctionIDNotFoundError
19+
from models_library.functions_errors import (
20+
FunctionIDNotFoundError,
21+
FunctionReadAccessDeniedError,
22+
)
2123
from models_library.rest_pagination import PageMetaInfoLimitOffset
2224
from servicelib.aiohttp import status
2325
from simcore_service_api_server._meta import API_VTAG
@@ -92,8 +94,35 @@ async def test_get_function_not_found(
9294
None,
9395
FunctionIDNotFoundError(function_id=non_existent_function_id),
9496
)
95-
with pytest.raises(FunctionIDNotFoundError):
96-
await client.get(f"{API_VTAG}/functions/{non_existent_function_id}", auth=auth)
97+
response = await client.get(
98+
f"{API_VTAG}/functions/{non_existent_function_id}", auth=auth
99+
)
100+
assert response.status_code == status.HTTP_404_NOT_FOUND
101+
102+
103+
async def test_get_function_read_access_denied(
104+
client: AsyncClient,
105+
mock_handler_in_functions_rpc_interface: Callable[
106+
[str, Any, Exception | None], None
107+
],
108+
mock_registered_function: RegisteredProjectFunction,
109+
auth: httpx.BasicAuth,
110+
) -> None:
111+
unauthorized_user_id = "unauthorized user"
112+
mock_handler_in_functions_rpc_interface(
113+
"get_function",
114+
None,
115+
FunctionReadAccessDeniedError(
116+
function_id=mock_registered_function.uid, user_id=unauthorized_user_id
117+
),
118+
)
119+
response = await client.get(
120+
f"{API_VTAG}/functions/{mock_registered_function.uid}", auth=auth
121+
)
122+
assert response.status_code == status.HTTP_403_FORBIDDEN
123+
assert response.json()["errors"][0] == (
124+
f"Function {mock_registered_function.uid} read access denied for user {unauthorized_user_id}"
125+
)
97126

98127

99128
async def test_list_functions(

0 commit comments

Comments
 (0)