Skip to content

Commit b8a236c

Browse files
committed
many fixes
1 parent f8edbb3 commit b8a236c

File tree

14 files changed

+178
-156
lines changed

14 files changed

+178
-156
lines changed

packages/pytest-simcore/src/pytest_simcore/helpers/httpx_assert_checks.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ def assert_status(
4444
error = validated_response.error
4545
if is_error(expected_status_code):
4646
_do_assert_error(
47-
data, error, expected_status_code, expected_msg, expected_error_code
47+
data,
48+
error,
49+
expected_status_code,
50+
expected_msg,
51+
expected_error_code,
4852
)
4953
return data, error
5054

@@ -60,8 +64,8 @@ def _do_assert_error(
6064
data,
6165
error,
6266
expected_status_code: int,
63-
expected_msg: str | None = None,
64-
expected_error_code: str | None = None,
67+
expected_msg: str | None,
68+
expected_error_code: str | None,
6569
):
6670
assert not data, pformat(data)
6771
assert error, pformat(error)

packages/pytest-simcore/src/pytest_simcore/helpers/s3.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
from typing import Final
55

66
import aiofiles
7+
import httpx
78
import orjson
8-
from aiohttp import ClientSession
99
from aws_library.s3 import MultiPartUploadLinks
10+
from fastapi import status
1011
from models_library.api_schemas_storage import ETag, FileUploadSchema, UploadedPart
1112
from pydantic import AnyUrl, ByteSize, TypeAdapter
12-
from servicelib.aiohttp import status
1313
from servicelib.utils import limited_as_completed, logged_gather
1414
from types_aiobotocore_s3 import S3Client
1515

@@ -37,7 +37,7 @@ async def _file_sender(
3737

3838

3939
async def upload_file_part(
40-
session: ClientSession,
40+
session: httpx.AsyncClient,
4141
file: Path,
4242
part_index: int,
4343
file_offset: int,
@@ -48,11 +48,11 @@ async def upload_file_part(
4848
raise_while_uploading: bool = False,
4949
) -> tuple[int, ETag]:
5050
print(
51-
f"--> uploading {this_file_chunk_size=} of {file=}, [{part_index+1}/{num_parts}]..."
51+
f"--> uploading {this_file_chunk_size=} of {file=}, [{part_index + 1}/{num_parts}]..."
5252
)
5353
response = await session.put(
5454
str(upload_url),
55-
data=_file_sender(
55+
content=_file_sender(
5656
file,
5757
offset=file_offset,
5858
bytes_to_send=this_file_chunk_size,
@@ -64,12 +64,12 @@ async def upload_file_part(
6464
)
6565
response.raise_for_status()
6666
# NOTE: the response from minio does not contain a json body
67-
assert response.status == status.HTTP_200_OK
67+
assert response.status_code == status.HTTP_200_OK
6868
assert response.headers
6969
assert "Etag" in response.headers
7070
received_e_tag = orjson.loads(response.headers["Etag"])
7171
print(
72-
f"--> completed upload {this_file_chunk_size=} of {file=}, [{part_index+1}/{num_parts}], {received_e_tag=}"
72+
f"--> completed upload {this_file_chunk_size=} of {file=}, [{part_index + 1}/{num_parts}], {received_e_tag=}"
7373
)
7474
return (part_index, received_e_tag)
7575

@@ -80,7 +80,7 @@ async def upload_file_to_presigned_link(
8080
file_size = file.stat().st_size
8181

8282
with log_context(logging.INFO, msg=f"uploading {file} via {file_upload_link=}"):
83-
async with ClientSession() as session:
83+
async with httpx.AsyncClient() as session:
8484
file_chunk_size = int(file_upload_link.chunk_size)
8585
num_urls = len(file_upload_link.urls)
8686
last_chunk_size = file_size - file_chunk_size * (num_urls - 1)
@@ -100,7 +100,7 @@ async def upload_file_to_presigned_link(
100100
upload_url,
101101
)
102102
)
103-
results = await logged_gather(*upload_tasks, max_concurrency=0)
103+
results = await logged_gather(*upload_tasks, max_concurrency=20)
104104
return [UploadedPart(number=index + 1, e_tag=e_tag) for index, e_tag in results]
105105

106106

packages/service-library/src/servicelib/fastapi/http_error.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,28 @@
11
from collections.abc import Awaitable, Callable
22

3-
from fastapi import HTTPException, status
3+
from fastapi import HTTPException
44
from fastapi.encoders import jsonable_encoder
5-
from fastapi.exceptions import RequestValidationError
65
from fastapi.openapi.constants import REF_PREFIX
76
from fastapi.openapi.utils import validation_error_response_definition
87
from fastapi.requests import Request
98
from fastapi.responses import JSONResponse
10-
from pydantic import ValidationError
119

1210

13-
async def http_error_handler(_: Request, exc: Exception) -> JSONResponse:
14-
assert isinstance(exc, HTTPException)
15-
16-
return JSONResponse(
17-
content=jsonable_encoder({"errors": [exc.detail]}), status_code=exc.status_code
18-
)
19-
11+
def make_default_http_error_handler(
12+
*, envelope_error: bool
13+
) -> Callable[[Request, Exception], Awaitable[JSONResponse]]:
14+
async def _http_error_handler(_: Request, exc: Exception) -> JSONResponse:
15+
assert isinstance(exc, HTTPException)
2016

21-
async def http422_error_handler(
22-
request: Request,
23-
exc: Exception,
24-
) -> JSONResponse:
25-
assert request # nosec
26-
assert isinstance(exc, RequestValidationError | ValidationError)
17+
error_content = {"errors": [exc.detail]}
18+
if envelope_error:
19+
error_content = {"error": error_content}
20+
return JSONResponse(
21+
content=jsonable_encoder(error_content),
22+
status_code=exc.status_code,
23+
)
2724

28-
return JSONResponse(
29-
content=jsonable_encoder({"errors": exc.errors()}),
30-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
31-
)
25+
return _http_error_handler
3226

3327

3428
validation_error_response_definition["properties"] = {
@@ -41,7 +35,10 @@ async def http422_error_handler(
4135

4236

4337
def make_http_error_handler_for_exception(
44-
status_code: int, exception_cls: type[BaseException]
38+
status_code: int,
39+
exception_cls: type[BaseException],
40+
*,
41+
envelope_error: bool,
4542
) -> Callable[[Request, Exception], Awaitable[JSONResponse]]:
4643
"""
4744
Produces a handler for BaseException-type exceptions which converts them
@@ -52,8 +49,11 @@ def make_http_error_handler_for_exception(
5249

5350
async def _http_error_handler(_: Request, exc: Exception) -> JSONResponse:
5451
assert isinstance(exc, exception_cls) # nosec
52+
error_content = {"errors": [f"{exc}"]}
53+
if envelope_error:
54+
error_content = {"error": error_content}
5555
return JSONResponse(
56-
content=jsonable_encoder({"errors": [str(exc)]}),
56+
content=jsonable_encoder(error_content),
5757
status_code=status_code,
5858
)
5959

services/storage/src/simcore_service_storage/api/rest/_files.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import logging
33
import socket
4+
import urllib.parse
45
from typing import Annotated, cast
56

67
from fastapi import APIRouter, Depends, Header, HTTPException, Request
@@ -301,7 +302,9 @@ async def is_completed_upload_file(
301302
# if it returns slow we return a 202 - Accepted, the client will have to check later
302303
# for completeness
303304
task_name = create_upload_completion_task_name(query_params.user_id, file_id)
304-
assert task_name == future_id # nosec
305+
assert task_name == urllib.parse.quote(
306+
future_id
307+
) # nosec # NOTE: fastapi auto-decode path parameters
305308
# first check if the task is in the app
306309
if task := get_completed_upload_tasks(request.app).get(task_name):
307310
if task.done():

services/storage/src/simcore_service_storage/exceptions/handlers.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from aws_library.s3 import S3AccessError, S3KeyNotFoundError
55
from fastapi import FastAPI, HTTPException, status
66
from fastapi.exceptions import RequestValidationError
7+
from pydantic import ValidationError
78
from servicelib.fastapi.http_error import (
8-
http422_error_handler,
9-
http_error_handler,
9+
make_default_http_error_handler,
1010
make_http_error_handler_for_exception,
1111
)
1212

@@ -26,13 +26,34 @@
2626

2727

2828
def set_exception_handlers(app: FastAPI) -> None:
29-
app.add_exception_handler(HTTPException, http_error_handler)
30-
app.add_exception_handler(RequestValidationError, http422_error_handler)
31-
# director-v2 core.errors mappend into HTTP errors
29+
app.add_exception_handler(
30+
HTTPException, make_default_http_error_handler(envelope_error=True)
31+
)
32+
app.add_exception_handler(
33+
HTTPException,
34+
make_http_error_handler_for_exception(
35+
status.HTTP_422_UNPROCESSABLE_ENTITY,
36+
HTTPException,
37+
envelope_error=True,
38+
),
39+
)
40+
41+
for exc_unprocessable in (ValidationError, RequestValidationError):
42+
app.add_exception_handler(
43+
exc_unprocessable,
44+
make_http_error_handler_for_exception(
45+
status.HTTP_422_UNPROCESSABLE_ENTITY,
46+
exc_unprocessable,
47+
envelope_error=True,
48+
),
49+
)
50+
3251
app.add_exception_handler(
3352
InvalidFileIdentifierError,
3453
make_http_error_handler_for_exception(
35-
status.HTTP_422_UNPROCESSABLE_ENTITY, InvalidFileIdentifierError
54+
status.HTTP_422_UNPROCESSABLE_ENTITY,
55+
InvalidFileIdentifierError,
56+
envelope_error=True,
3657
),
3758
)
3859
for exc_not_found in (
@@ -43,7 +64,7 @@ def set_exception_handlers(app: FastAPI) -> None:
4364
app.add_exception_handler(
4465
exc_not_found,
4566
make_http_error_handler_for_exception(
46-
status.HTTP_404_NOT_FOUND, exc_not_found
67+
status.HTTP_404_NOT_FOUND, exc_not_found, envelope_error=True
4768
),
4869
)
4970
for exc_access in (
@@ -53,13 +74,15 @@ def set_exception_handlers(app: FastAPI) -> None:
5374
app.add_exception_handler(
5475
exc_access,
5576
make_http_error_handler_for_exception(
56-
status.HTTP_403_FORBIDDEN, exc_access
77+
status.HTTP_403_FORBIDDEN, exc_access, envelope_error=True
5778
),
5879
)
5980
app.add_exception_handler(
6081
LinkAlreadyExistsError,
6182
make_http_error_handler_for_exception(
62-
status.HTTP_422_UNPROCESSABLE_ENTITY, LinkAlreadyExistsError
83+
status.HTTP_422_UNPROCESSABLE_ENTITY,
84+
LinkAlreadyExistsError,
85+
envelope_error=True,
6386
),
6487
)
6588
for exc_3rd_party in (
@@ -69,27 +92,29 @@ def set_exception_handlers(app: FastAPI) -> None:
6992
app.add_exception_handler(
7093
exc_3rd_party,
7194
make_http_error_handler_for_exception(
72-
status.HTTP_503_SERVICE_UNAVAILABLE, exc_3rd_party
95+
status.HTTP_503_SERVICE_UNAVAILABLE, exc_3rd_party, envelope_error=True
7396
),
7497
)
7598

7699
app.add_exception_handler(
77100
DatcoreAdapterTimeoutError,
78101
make_http_error_handler_for_exception(
79-
status.HTTP_504_GATEWAY_TIMEOUT, DatcoreAdapterTimeoutError
102+
status.HTTP_504_GATEWAY_TIMEOUT,
103+
DatcoreAdapterTimeoutError,
104+
envelope_error=True,
80105
),
81106
)
82107

83108
# SEE https://docs.python.org/3/library/exceptions.html#exception-hierarchy
84109
app.add_exception_handler(
85110
NotImplementedError,
86111
make_http_error_handler_for_exception(
87-
status.HTTP_501_NOT_IMPLEMENTED, NotImplementedError
112+
status.HTTP_501_NOT_IMPLEMENTED, NotImplementedError, envelope_error=True
88113
),
89114
)
90115
app.add_exception_handler(
91116
Exception,
92117
make_http_error_handler_for_exception(
93-
status.HTTP_500_INTERNAL_SERVER_ERROR, Exception
118+
status.HTTP_500_INTERNAL_SERVER_ERROR, Exception, envelope_error=True
94119
),
95120
)

services/storage/src/simcore_service_storage/models.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,6 @@ class SyncMetadataResponse(BaseModel):
186186
class FileDownloadQueryParams(StorageQueryParamsBase):
187187
link_type: LinkType = LinkType.PRESIGNED
188188

189-
@field_validator("link_type", mode="before")
190-
@classmethod
191-
def convert_from_lower_case(cls, v: str) -> str:
192-
if v is not None:
193-
return f"{v}".upper()
194-
return v
195-
196189

197190
class FileDownloadResponse(BaseModel):
198191
link: AnyUrl

services/storage/src/simcore_service_storage/modules/db/file_meta_data.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ async def upsert(
4040
index_elements=[file_meta_data.c.file_id], set_=fmd_db.model_dump()
4141
).returning(literal_column("*"))
4242
result = await conn.execute(on_update_statement)
43-
await conn.commit()
4443
row = result.one()
4544
return FileMetaDataAtDB.model_validate(row)
4645

services/storage/src/simcore_service_storage/simcore_s3_dsm.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -445,15 +445,15 @@ async def create_file_download_link(
445445
directory_file_id: SimcoreS3FileID | None = await get_directory_file_id(
446446
conn, cast(SimcoreS3FileID, file_id)
447447
)
448-
await self.__ensure_read_access_rights(
448+
await self._ensure_read_access_rights(
449449
conn, user_id, directory_file_id if directory_file_id else file_id
450450
)
451451
if directory_file_id:
452452
if not await get_s3_client(self.app).object_exists(
453453
bucket=self.simcore_bucket_name, object_key=f"{file_id}"
454454
):
455455
raise S3KeyNotFoundError(key=file_id, bucket=self.simcore_bucket_name)
456-
return await self.__get_link(
456+
return await self._get_link(
457457
TypeAdapter(SimcoreS3FileID).validate_python(file_id), link_type
458458
)
459459
# standard file link
@@ -464,10 +464,10 @@ async def create_file_download_link(
464464
if not is_file_entry_valid(fmd):
465465
# try lazy update
466466
fmd = await self._update_database_from_storage(fmd)
467-
return await self.__get_link(fmd.object_name, link_type)
467+
return await self._get_link(fmd.object_name, link_type)
468468

469469
@staticmethod
470-
async def __ensure_read_access_rights(
470+
async def _ensure_read_access_rights(
471471
conn: AsyncConnection, user_id: UserID, storage_file_id: StorageFileID
472472
) -> None:
473473
can = await get_file_access_rights(conn, user_id, storage_file_id)
@@ -478,7 +478,7 @@ async def __ensure_read_access_rights(
478478
#
479479
raise FileAccessRightError(access_right="read", file_id=storage_file_id)
480480

481-
async def __get_link(
481+
async def _get_link(
482482
self, s3_file_id: SimcoreS3FileID, link_type: LinkType
483483
) -> AnyUrl:
484484
link: AnyUrl = TypeAdapter(AnyUrl).validate_python(

services/storage/tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,8 @@ async def _directory_creator(dir_name: str):
473473
# 3. call file_upload_complete_response until it replies OK
474474

475475
directory_file_id = create_simcore_file_id(project_id, node_id, dir_name)
476-
directory_file_upload: FileUploadSchema = await create_upload_file_link_v2(
477-
directory_file_id, link_type="s3", is_directory="true", file_size=-1
476+
directory_file_upload = await create_upload_file_link_v2(
477+
directory_file_id, link_type="S3", is_directory="true", file_size=-1
478478
)
479479
# always returns a v2 link when dealing with directories
480480
assert isinstance(directory_file_upload, FileUploadSchema)

0 commit comments

Comments
 (0)