Skip to content

Commit 73eff71

Browse files
authored
♻️Storage: light refactoring to reduce noise (#7233)
1 parent be9f062 commit 73eff71

19 files changed

+192
-135
lines changed

packages/pytest-simcore/src/pytest_simcore/file_extra.py

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66
from faker import Faker
7-
from pydantic import ByteSize
7+
from pydantic import ByteSize, NonNegativeInt
88
from pytest_simcore.helpers.logging_tools import log_context
99

1010

@@ -26,6 +26,37 @@ def _creator(size: ByteSize, name: str | None = None) -> Path:
2626
return _creator
2727

2828

29+
def _create_random_content(
30+
faker: Faker,
31+
*,
32+
base_dir: Path,
33+
file_min_size: ByteSize,
34+
file_max_size: ByteSize,
35+
remaining_size: ByteSize,
36+
depth: NonNegativeInt | None,
37+
) -> ByteSize:
38+
if remaining_size <= 0:
39+
return remaining_size
40+
41+
file_size = ByteSize(
42+
faker.pyint(
43+
min_value=min(file_min_size, remaining_size),
44+
max_value=min(remaining_size, file_max_size),
45+
)
46+
)
47+
if depth is None:
48+
depth = faker.pyint(0, 5)
49+
file_path = base_dir / f"{faker.unique.file_path(depth=depth, absolute=False)}"
50+
file_path.parent.mkdir(parents=True, exist_ok=True)
51+
assert not file_path.exists()
52+
with file_path.open("wb") as fp:
53+
fp.write(f"I am a {file_size.human_readable()} file".encode())
54+
fp.truncate(file_size)
55+
assert file_path.exists()
56+
57+
return ByteSize(remaining_size - file_size)
58+
59+
2960
@pytest.fixture
3061
def create_folder_of_size_with_multiple_files(
3162
tmp_path: Path, faker: Faker
@@ -34,33 +65,12 @@ def _create_folder_of_size_with_multiple_files(
3465
directory_size: ByteSize,
3566
file_min_size: ByteSize,
3667
file_max_size: ByteSize,
68+
depth: NonNegativeInt | None = None,
3769
) -> Path:
3870
# Helper function to create random files and directories
3971
assert file_min_size > 0
4072
assert file_min_size <= file_max_size
4173

42-
def create_random_content(base_dir: Path, remaining_size: ByteSize) -> ByteSize:
43-
if remaining_size <= 0:
44-
return remaining_size
45-
46-
# Decide to create a file or a subdirectory
47-
# Create a file
48-
file_size = ByteSize(
49-
faker.pyint(
50-
min_value=min(file_min_size, remaining_size),
51-
max_value=min(remaining_size, file_max_size),
52-
)
53-
) # max file size 1MB
54-
file_path = base_dir / f"{faker.file_path(depth=4, absolute=False)}"
55-
file_path.parent.mkdir(parents=True, exist_ok=True)
56-
assert not file_path.exists()
57-
with file_path.open("wb") as fp:
58-
fp.write(f"I am a {file_size.human_readable()} file".encode())
59-
fp.truncate(file_size)
60-
assert file_path.exists()
61-
62-
return ByteSize(remaining_size - file_size)
63-
6474
# Recursively create content in the temporary directory
6575
remaining_size = directory_size
6676
with log_context(
@@ -70,7 +80,14 @@ def create_random_content(base_dir: Path, remaining_size: ByteSize) -> ByteSize:
7080
) as ctx:
7181
num_files_created = 0
7282
while remaining_size > 0:
73-
remaining_size = create_random_content(tmp_path, remaining_size)
83+
remaining_size = _create_random_content(
84+
faker,
85+
base_dir=tmp_path,
86+
file_min_size=file_min_size,
87+
file_max_size=file_max_size,
88+
remaining_size=remaining_size,
89+
depth=depth,
90+
)
7491
num_files_created += 1
7592
ctx.logger.info("created %s files", num_files_created)
7693
return tmp_path

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def assert_status(
2020
response_model: type[T] | None,
2121
*,
2222
expected_msg: str | None = None,
23-
is_enveloped: bool = True,
23+
expect_envelope: bool = True,
2424
) -> tuple[T | None, Any]:
2525
"""
2626
Asserts for enveloped responses
@@ -36,7 +36,7 @@ def assert_status(
3636
if expected_status_code == status.HTTP_204_NO_CONTENT:
3737
assert response.text == ""
3838
return None, None
39-
if is_enveloped:
39+
if expect_envelope:
4040
validated_response = TypeAdapter(Envelope[response_model]).validate_json(
4141
response.text
4242
)
@@ -49,6 +49,8 @@ def assert_status(
4949
expected_status_code,
5050
expected_msg,
5151
)
52+
else:
53+
assert data is not None
5254
return data, error
5355

5456
if is_error(expected_status_code):

services/storage/tests/helpers/utils.py renamed to packages/pytest-simcore/src/pytest_simcore/helpers/storage_utils.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import logging
22
import os
3-
from typing import Any
3+
from pathlib import Path
4+
from typing import Any, TypedDict
45

56
import sqlalchemy as sa
7+
from models_library.basic_types import SHA256Str
68
from simcore_postgres_database.storage_models import projects
79
from sqlalchemy.ext.asyncio import AsyncEngine
810

@@ -24,6 +26,10 @@ async def get_updated_project(
2426
result = await conn.execute(
2527
sa.select(projects).where(projects.c.uuid == project_id)
2628
)
27-
row = result.fetchone()
28-
assert row
29+
row = result.one()
2930
return row._asdict()
31+
32+
33+
class FileIDDict(TypedDict):
34+
path: Path
35+
sha256_checksum: SHA256Str

services/storage/tests/fixtures/data_models.py renamed to packages/pytest-simcore/src/pytest_simcore/simcore_storage_data_models.py

Lines changed: 77 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,24 @@
77
from contextlib import asynccontextmanager
88
from pathlib import Path
99
from random import choice, randint
10-
from typing import Any
10+
from typing import Any, cast
1111

1212
import pytest
1313
import sqlalchemy as sa
1414
from faker import Faker
1515
from models_library.basic_types import SHA256Str
1616
from models_library.projects import ProjectID
17-
from models_library.projects_nodes_io import NodeID, SimcoreS3FileID
17+
from models_library.projects_nodes_io import NodeID, SimcoreS3FileID, StorageFileID
1818
from models_library.users import UserID
1919
from pydantic import ByteSize, TypeAdapter
20-
from pytest_simcore.helpers.faker_factories import random_project, random_user
2120
from servicelib.utils import limited_gather
2221
from simcore_postgres_database.models.project_to_groups import project_to_groups
2322
from simcore_postgres_database.storage_models import projects, users
2423
from sqlalchemy.dialects.postgresql import insert as pg_insert
2524
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine
2625

27-
from ..helpers.utils import get_updated_project
26+
from .helpers.faker_factories import random_project, random_user
27+
from .helpers.storage_utils import FileIDDict, get_updated_project
2828

2929

3030
@asynccontextmanager
@@ -259,6 +259,39 @@ async def _creator(
259259
return _creator
260260

261261

262+
async def _upload_file_and_update_project(
263+
project_id: ProjectID,
264+
node_id: NodeID,
265+
*,
266+
file_name: str | None,
267+
file_id: StorageFileID | None,
268+
file_sizes: tuple[ByteSize, ...],
269+
file_checksums: tuple[SHA256Str, ...],
270+
node_to_files_mapping: dict[NodeID, dict[SimcoreS3FileID, FileIDDict]],
271+
upload_file: Callable[..., Awaitable[tuple[Path, SimcoreS3FileID]]],
272+
create_simcore_file_id: Callable[
273+
[ProjectID, NodeID, str, Path | None], SimcoreS3FileID
274+
],
275+
faker: Faker,
276+
) -> None:
277+
if file_name is None:
278+
file_name = faker.file_name()
279+
file_id = create_simcore_file_id(project_id, node_id, file_name, None)
280+
checksum: SHA256Str = choice(file_checksums) # noqa: S311
281+
src_file, _ = await upload_file(
282+
file_size=choice(file_sizes), # noqa: S311
283+
file_name=file_name,
284+
file_id=file_id,
285+
sha256_checksum=checksum,
286+
)
287+
assert file_name is not None
288+
assert file_id is not None
289+
node_to_files_mapping[node_id][file_id] = {
290+
"path": src_file,
291+
"sha256_checksum": checksum,
292+
}
293+
294+
262295
@pytest.fixture
263296
async def random_project_with_files(
264297
sqlalchemy_async_engine: AsyncEngine,
@@ -271,11 +304,7 @@ async def random_project_with_files(
271304
faker: Faker,
272305
) -> Callable[
273306
[int, tuple[ByteSize, ...], tuple[SHA256Str, ...]],
274-
Awaitable[
275-
tuple[
276-
dict[str, Any], dict[NodeID, dict[SimcoreS3FileID, dict[str, Path | str]]]
277-
]
278-
],
307+
Awaitable[tuple[dict[str, Any], dict[NodeID, dict[SimcoreS3FileID, FileIDDict]]]],
279308
]:
280309
async def _creator(
281310
num_nodes: int = 12,
@@ -295,76 +324,67 @@ async def _creator(
295324
"488f3b57932803bbf644593bd46d95599b1d4da1d63bc020d7ebe6f1c255f7f3"
296325
),
297326
),
298-
) -> tuple[
299-
dict[str, Any], dict[NodeID, dict[SimcoreS3FileID, dict[str, Path | str]]]
300-
]:
327+
) -> tuple[dict[str, Any], dict[NodeID, dict[SimcoreS3FileID, FileIDDict]]]:
301328
assert len(file_sizes) == len(file_checksums)
302329
project = await create_project(name="random-project")
303-
src_projects_list: dict[
304-
NodeID, dict[SimcoreS3FileID, dict[str, Path | str]]
305-
] = {}
330+
node_to_files_mapping: dict[NodeID, dict[SimcoreS3FileID, FileIDDict]] = {}
306331
upload_tasks: deque[Awaitable] = deque()
307332
for _node_index in range(num_nodes):
308-
# NOTE: we put some more outputs in there to simulate a real case better
309-
new_node_id = NodeID(f"{faker.uuid4()}")
333+
# Create a node with outputs (files and others)
334+
project_id = ProjectID(project["uuid"])
335+
node_id = cast(NodeID, faker.uuid4(cast_to=None))
336+
output3_file_name = faker.file_name()
310337
output3_file_id = create_simcore_file_id(
311-
ProjectID(project["uuid"]),
312-
new_node_id,
313-
faker.file_name(),
314-
Path("outputs/output3"),
338+
project_id, node_id, output3_file_name, Path("outputs/output_3")
315339
)
316-
src_node_id = await create_project_node(
340+
created_node_id = await create_project_node(
317341
ProjectID(project["uuid"]),
318-
new_node_id,
342+
node_id,
319343
outputs={
320344
"output_1": faker.pyint(),
321345
"output_2": faker.pystr(),
322346
"output_3": f"{output3_file_id}",
323347
},
324348
)
325-
assert src_node_id == new_node_id
326-
327-
# upload the output 3 and some random other files at the root of each node
328-
src_projects_list[src_node_id] = {}
329-
checksum: SHA256Str = choice(file_checksums) # noqa: S311
330-
src_file, _ = await upload_file(
331-
file_size=choice(file_sizes), # noqa: S311
332-
file_name=Path(output3_file_id).name,
333-
file_id=output3_file_id,
334-
sha256_checksum=checksum,
335-
)
336-
src_projects_list[src_node_id][output3_file_id] = {
337-
"path": src_file,
338-
"sha256_checksum": checksum,
339-
}
340-
341-
async def _upload_file_and_update_project(project, src_node_id):
342-
src_file_name = faker.file_name()
343-
src_file_uuid = create_simcore_file_id(
344-
ProjectID(project["uuid"]), src_node_id, src_file_name, None
345-
)
346-
checksum: SHA256Str = choice(file_checksums) # noqa: S311
347-
src_file, _ = await upload_file(
348-
file_size=choice(file_sizes), # noqa: S311
349-
file_name=src_file_name,
350-
file_id=src_file_uuid,
351-
sha256_checksum=checksum,
349+
assert created_node_id == node_id
350+
351+
node_to_files_mapping[created_node_id] = {}
352+
upload_tasks.append(
353+
_upload_file_and_update_project(
354+
project_id,
355+
node_id,
356+
file_name=output3_file_name,
357+
file_id=output3_file_id,
358+
file_sizes=file_sizes,
359+
file_checksums=file_checksums,
360+
upload_file=upload_file,
361+
create_simcore_file_id=create_simcore_file_id,
362+
faker=faker,
363+
node_to_files_mapping=node_to_files_mapping,
352364
)
353-
src_projects_list[src_node_id][src_file_uuid] = {
354-
"path": src_file,
355-
"sha256_checksum": checksum,
356-
}
365+
)
357366

358-
# add a few random files in the node storage
367+
# add a few random files in the node workspace
359368
upload_tasks.extend(
360369
[
361-
_upload_file_and_update_project(project, src_node_id)
370+
_upload_file_and_update_project(
371+
project_id,
372+
node_id,
373+
file_name=None,
374+
file_id=None,
375+
file_sizes=file_sizes,
376+
file_checksums=file_checksums,
377+
upload_file=upload_file,
378+
create_simcore_file_id=create_simcore_file_id,
379+
faker=faker,
380+
node_to_files_mapping=node_to_files_mapping,
381+
)
362382
for _ in range(randint(0, 3)) # noqa: S311
363383
]
364384
)
365385
await limited_gather(*upload_tasks, limit=10)
366386

367387
project = await get_updated_project(sqlalchemy_async_engine, project["uuid"])
368-
return project, src_projects_list
388+
return project, node_to_files_mapping
369389

370390
return _creator

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def set_app_default_http_error_handlers(app: FastAPI) -> None:
9494
app.add_exception_handler(
9595
ValidationError,
9696
make_http_error_handler_for_exception(
97-
status.HTTP_422_UNPROCESSABLE_ENTITY,
97+
status.HTTP_500_INTERNAL_SERVER_ERROR,
9898
ValidationError,
9999
envelope_error=True,
100100
),

services/storage/requirements/_base.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ httpx
2121
opentelemetry-instrumentation-botocore
2222
packaging
2323
fastapi[all]
24+
fastapi-pagination
2425
orjson
2526
pydantic[dotenv]
2627
tenacity

0 commit comments

Comments
 (0)