Skip to content

Conversation

@dkeller9
Copy link
Contributor

@dkeller9 dkeller9 commented Oct 9, 2025

This endpoint runs pynwb validation on nwb files and returns an error if they do not pass.

Copy link
Collaborator

@james-isbister james-isbister left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two comments:

  1. I'm not familiar with this await calling another async function. Perhaps @GianlucaFicarelli could comment on whether this part looks ok?
  2. If I understand correctly, the validation is passed if the file can be opened? _process_nwb and _validate_and_read_nwb_file look like they're doing quite similar things in this regard. Could you clarrify the difference? And do they need to be in two separate async functions.

Thanks!

@dkeller9
Copy link
Contributor Author

@james-isbister _process_nwb and _validate_and_read_nwb_file have now been consolidated.


test_all_nwb_readers(temp_file_path_local)

return temp_file_path_local # Return the path for cleanup
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the temp file isn't automatically deleted in the context manager, instead of returning the path?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file path (temp_file_path_local) is returned from blocking_file_operations and stored in the main function's temp_file_path variable. It is then explicitly and reliably deleted in the finally block of validate_nwb_file via the helper function _cleanup_file(temp_file_path).This manual cleanup is essential because the file needs to persist on the disk while the test_all_nwb_readers function runs on it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's enough to move validate_all_nwb_readers inside the context manager to avoid all this complexity, since the file is needed only by that function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, it has now been moved to the context manager to avoid having to explicitly delete temp files.

summary="Validate new format.",
description="Tests a new file (.nwb) with basic validation.",
)
async def validate_nwb_file(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defining this endpoint as async doesn't provide any advantage because it still needs to use threads, and it makes the logic more complicated.
It should be defined as a regular sync endpoint, similarly to the other endpoints in obi-one handled by fastapi.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I have made it sync again.

# 2. Run the synchronous validation function
try:
# This function now runs synchronously and cleanup is handled
# automatically when exiting the 'with' block.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and other comments that seem to have been added automatically should be removed because useless. I didn't mark all of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They have been removed.


NWB_READERS = [BBPNWBReader, ScalaNWBReader, AIBSNWBReader, TRTNWBReader] # , VUNWBReader]

test_protocols = ["APThreshold", "SAPThres1", "SAPThres2", "SAPThres3", "SAPTres1",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is a constants and should be upper case.

Note also that this file doesn't look to be formatted with make format and make lint. After formatting, it would be even clearer to move this big list into a separate file (constants.py or something similar, without any code).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to upper case and reformatted.


# List of all available NWB Reader classes to iterate over

NWB_READERS = [BBPNWBReader, ScalaNWBReader, AIBSNWBReader, TRTNWBReader] # , VUNWBReader]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VUNWBReader is commented out, but it should be uncommented or removed, or explained.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has been removed.

def _handle_empty_file(file: UploadFile) -> None:
"""Handle empty file upload by raising an appropriate HTTPException."""
L.error(f"Empty file uploaded: {file.filename}")
raise HTTPException(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with most of the other code, it should raise ApiError instead of HTTPException.
Moreover, ApiErrorCode.BAD_REQUEST doesn't exist, but a more specific error code can be defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApiError is now used.

)
def validate_nwb_file(
file: Annotated[UploadFile, File(description="Nwb file to upload (.swc, .h5, or .asc)")],
) -> dict:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a generic dict, the endpoint should return a pydantic schema. This has also the advantage to appear in the openapi docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A pydantic schema is now returned.

return {
"status": "success",
"message": "NWB file validation successful.",
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to the code above, but there should be unit tests for testing the new endpoint.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit tests added.

Comment on lines +472 to +479
# File extension is needed for the NWB readers to recognize the file type
file_extension = Path(file.filename).suffix.lower() if file.filename else ""
if file_extension != ".nwb":
raise ApiError(
message="Invalid file extension. Must be .nwb",
error_code=ApiErrorCode.INVALID_REQUEST,
http_status_code=HTTPStatus.BAD_REQUEST,
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check can be moved at the beginning, so the file isn't even read if the extension is incorrect.

L.error(f"Error reading uploaded file content: {e!s}")
raise ApiError(
message=f"Error reading uploaded file content: {e!s}",
error_code=ApiErrorCode.INTERNAL_SERVER_ERROR,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApiErrorCode.INTERNAL_SERVER_ERROR doesn't exist, it can be GENERIC_ERROR

L.error(f"Error writing file to disk: {e!s}")
raise ApiError(
message=f"Error writing file to disk: {e!s}",
error_code=ApiErrorCode.INTERNAL_SERVER_ERROR,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApiErrorCode.INTERNAL_SERVER_ERROR doesn't exist, it can be GENERIC_ERROR

files created during processing are automatically cleaned up.
"""
try:
content = file.file.read()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the logic below, we don't need to load the content in memory here, since the content is just saved to a temporary file below.
It should be possible to unify the two blocks of code reading and writing, so that the temp file is written directly from the stream.

"""
for readerclass in NWB_READERS:
try:
reader = readerclass(nwb_file_path, TEST_PROTOCOLS)
Copy link
Collaborator

@GianlucaFicarelli GianlucaFicarelli Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the classes defined in NWB_READERS, the __init__ wants a hdf5 object (h5py.File) and not a path.
I didn't try the code, does it work for you?
The function validate_all_nwb_readers isn't tested in test_validate_nwb.py because the function is patched, so it would be safer to add an explicit test with a small nwb file.

http_status_code=HTTPStatus.BAD_REQUEST,
)

with tempfile.TemporaryDirectory() as temp_dir:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the maximum expected file size for nwb files?
We should ensure that there is enough space, and it's also better to put a limit on the maximum file size.

response = client.post(
ROUTE,
# Note: Filenames are case-sensitive on the extension check unless lowercase() is used.
# Assuming the check is case-insensitive if .NWB is tested.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems wrong, since the check for the extension is case insensitive, but .NWB is expected to fail in this test.

@@ -0,0 +1,137 @@
from http import HTTPStatus
from unittest.mock import MagicMock, patch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are not run in the CI of this PR. Can you try to rebase or merge from main, to see if it's fixed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants