From 393b02e7d786eeeccd110425479c51cc0d2e79fa Mon Sep 17 00:00:00 2001 From: Aleksei Korota Date: Wed, 25 Mar 2026 17:30:15 +0300 Subject: [PATCH 1/5] fix: allow name to be None in BaseMetadata to handle API response --- aidial_client/types/metadata.py | 2 +- tests/test_response_processing.py | 110 ++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 tests/test_response_processing.py diff --git a/aidial_client/types/metadata.py b/aidial_client/types/metadata.py index 7662c20..874c529 100644 --- a/aidial_client/types/metadata.py +++ b/aidial_client/types/metadata.py @@ -17,7 +17,7 @@ class Config: alias_generator = to_camel allow_population_by_field_name = True - name: str + name: Optional[str] = None parent_path: Optional[str] = None bucket: str url: str diff --git a/tests/test_response_processing.py b/tests/test_response_processing.py new file mode 100644 index 0000000..a270df4 --- /dev/null +++ b/tests/test_response_processing.py @@ -0,0 +1,110 @@ +import json +from unittest.mock import MagicMock, patch + +import httpx +import pytest + +from aidial_client._exception import ParsingDataError +from aidial_client._utils._response_processing import process_block_response +from aidial_client.types.metadata import BaseMetadata, FileItem, FileMetadata + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# Exact payload returned by the real API (from the bug report) +REAL_API_PAYLOAD = { + "bucket": "684f6Lz7ubje66aoCRsa5c", + "items": [ + { + "bucket": "684f6Lz7ubje66aoCRsa5c", + "items": None, + "name": "appdata", + "nodeType": "FOLDER", + "parentPath": None, + "resourceType": "FILE", + "url": "files/684f6Lz7ubje66aoCRsa5c/appdata/", + }, + { + "bucket": "684f6Lz7ubje66aoCRsa5c", + "contentLength": 2207949, + "contentType": "application/pdf", + "name": "ontologies.pdf", + "nodeType": "ITEM", + "parentPath": None, + "resourceType": "FILE", + "updatedAt": 1770130629876, + "url": "files/684f6Lz7ubje66aoCRsa5c/ontologies.pdf", + }, + ], + "name": None, # <-- root folder has null name (Fix 2) + "nodeType": "FOLDER", + "parentPath": None, + "resourceType": "FILE", + "url": "files/684f6Lz7ubje66aoCRsa5c/", +} + + +def _make_response(payload: dict, status_code: int = 200) -> httpx.Response: + """Build a minimal httpx.Response carrying a JSON body.""" + raw = json.dumps(payload).encode() + return httpx.Response( + status_code=status_code, + headers={"content-type": "application/json"}, + content=raw, + ) + + +class TestBaseMetadataOptionalName: + """ + Before the fix, name: str caused a ValidationError when the API returned + "name": null for root-folder metadata entries. + """ + + def test_name_none_is_accepted(self): + """Root-folder entries come back with name=null; this must not raise.""" + meta = BaseMetadata( + name=None, + bucket="abc", + url="files/abc/", + node_type="FOLDER", + resource_type="FILE", + ) + assert meta.name is None + + def test_name_str_still_works(self): + meta = BaseMetadata( + name="my-file.pdf", + bucket="abc", + url="files/abc/my-file.pdf", + node_type="ITEM", + resource_type="FILE", + ) + assert meta.name == "my-file.pdf" + + def test_name_defaults_to_none_when_omitted(self): + meta = BaseMetadata( + bucket="abc", + url="files/abc/", + node_type="FOLDER", + resource_type="FILE", + ) + assert meta.name is None + + def test_file_metadata_name_none_via_model_validate(self): + """End-to-end: parse the real API payload – name=null must not fail.""" + result = ( + FileMetadata.model_validate(REAL_API_PAYLOAD) + if hasattr(FileMetadata, "model_validate") + else FileMetadata.parse_obj(REAL_API_PAYLOAD) + ) # type: ignore[attr-defined] + + assert result.name is None + + def test_file_metadata_full_real_payload(self): + """The full payload from the bug report must parse without error.""" + response = _make_response(REAL_API_PAYLOAD) + result = process_block_response(FileMetadata, response) + assert result.name is None + assert result.bucket == "684f6Lz7ubje66aoCRsa5c" + From f5223e428db93716e0f1e444423e2b85dc068f65 Mon Sep 17 00:00:00 2001 From: Aleksei Korota Date: Wed, 25 Mar 2026 17:32:57 +0300 Subject: [PATCH 2/5] fix: remove unused imports and clean up response processing tests --- tests/test_response_processing.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_response_processing.py b/tests/test_response_processing.py index a270df4..3695bb7 100644 --- a/tests/test_response_processing.py +++ b/tests/test_response_processing.py @@ -1,12 +1,9 @@ import json -from unittest.mock import MagicMock, patch import httpx -import pytest -from aidial_client._exception import ParsingDataError from aidial_client._utils._response_processing import process_block_response -from aidial_client.types.metadata import BaseMetadata, FileItem, FileMetadata +from aidial_client.types.metadata import BaseMetadata, FileMetadata # --------------------------------------------------------------------------- # Helpers @@ -107,4 +104,3 @@ def test_file_metadata_full_real_payload(self): result = process_block_response(FileMetadata, response) assert result.name is None assert result.bucket == "684f6Lz7ubje66aoCRsa5c" - From 390c5c0f34f43d53e53ffedd7840b9963e1cb114 Mon Sep 17 00:00:00 2001 From: Aleksei Korota Date: Wed, 25 Mar 2026 18:00:38 +0300 Subject: [PATCH 3/5] fix: update tests to handle None name in FileMetadata and improve response processing --- tests/test_response_processing.py | 161 ++++++++++++++---------------- 1 file changed, 77 insertions(+), 84 deletions(-) diff --git a/tests/test_response_processing.py b/tests/test_response_processing.py index 3695bb7..1ee5ca0 100644 --- a/tests/test_response_processing.py +++ b/tests/test_response_processing.py @@ -3,47 +3,10 @@ import httpx from aidial_client._utils._response_processing import process_block_response -from aidial_client.types.metadata import BaseMetadata, FileMetadata - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -# Exact payload returned by the real API (from the bug report) -REAL_API_PAYLOAD = { - "bucket": "684f6Lz7ubje66aoCRsa5c", - "items": [ - { - "bucket": "684f6Lz7ubje66aoCRsa5c", - "items": None, - "name": "appdata", - "nodeType": "FOLDER", - "parentPath": None, - "resourceType": "FILE", - "url": "files/684f6Lz7ubje66aoCRsa5c/appdata/", - }, - { - "bucket": "684f6Lz7ubje66aoCRsa5c", - "contentLength": 2207949, - "contentType": "application/pdf", - "name": "ontologies.pdf", - "nodeType": "ITEM", - "parentPath": None, - "resourceType": "FILE", - "updatedAt": 1770130629876, - "url": "files/684f6Lz7ubje66aoCRsa5c/ontologies.pdf", - }, - ], - "name": None, # <-- root folder has null name (Fix 2) - "nodeType": "FOLDER", - "parentPath": None, - "resourceType": "FILE", - "url": "files/684f6Lz7ubje66aoCRsa5c/", -} +from aidial_client.types.metadata import FileMetadata def _make_response(payload: dict, status_code: int = 200) -> httpx.Response: - """Build a minimal httpx.Response carrying a JSON body.""" raw = json.dumps(payload).encode() return httpx.Response( status_code=status_code, @@ -52,55 +15,85 @@ def _make_response(payload: dict, status_code: int = 200) -> httpx.Response: ) -class TestBaseMetadataOptionalName: - """ - Before the fix, name: str caused a ValidationError when the API returned - "name": null for root-folder metadata entries. - """ - - def test_name_none_is_accepted(self): - """Root-folder entries come back with name=null; this must not raise.""" - meta = BaseMetadata( - name=None, - bucket="abc", - url="files/abc/", - node_type="FOLDER", - resource_type="FILE", - ) - assert meta.name is None +BUCKET = "684f6Lz7ubje66aoCRsa5c" - def test_name_str_still_works(self): - meta = BaseMetadata( - name="my-file.pdf", - bucket="abc", - url="files/abc/my-file.pdf", - node_type="ITEM", - resource_type="FILE", - ) - assert meta.name == "my-file.pdf" - def test_name_defaults_to_none_when_omitted(self): - meta = BaseMetadata( - bucket="abc", - url="files/abc/", - node_type="FOLDER", - resource_type="FILE", - ) - assert meta.name is None - - def test_file_metadata_name_none_via_model_validate(self): - """End-to-end: parse the real API payload – name=null must not fail.""" - result = ( - FileMetadata.model_validate(REAL_API_PAYLOAD) - if hasattr(FileMetadata, "model_validate") - else FileMetadata.parse_obj(REAL_API_PAYLOAD) - ) # type: ignore[attr-defined] +class TestFileMetadataName: + def test_name_is_none(self): + """Root folder returned by the API has name=null; must parse without error.""" + payload = { + "bucket": BUCKET, + "items": [ + { + "bucket": BUCKET, + "items": None, + "name": "appdata", + "nodeType": "FOLDER", + "parentPath": None, + "resourceType": "FILE", + "url": f"files/{BUCKET}/appdata/", + } + ], + "name": None, + "nodeType": "FOLDER", + "parentPath": None, + "resourceType": "FILE", + "url": f"files/{BUCKET}/", + } + result = process_block_response(FileMetadata, _make_response(payload)) assert result.name is None + assert result.bucket == BUCKET + assert result.node_type == "FOLDER" + assert result.parent_path is None + assert result.resource_type == "FILE" + assert result.url == f"files/{BUCKET}/" + assert result.items is not None and len(result.items) == 1 + item = result.items[0] + assert item.name == "appdata" + assert item.bucket == BUCKET + assert item.node_type == "FOLDER" + assert item.parent_path is None + assert item.resource_type == "FILE" + assert item.url == f"files/{BUCKET}/appdata/" + + def test_name_is_string(self): + """Root folder whose single child is a file (non-null name).""" + payload = { + "bucket": BUCKET, + "items": [ + { + "bucket": BUCKET, + "contentLength": 2207949, + "contentType": "application/pdf", + "name": "ontologies.pdf", + "nodeType": "ITEM", + "parentPath": None, + "resourceType": "FILE", + "url": f"files/{BUCKET}/ontologies.pdf", + } + ], + "name": None, + "nodeType": "FOLDER", + "parentPath": None, + "resourceType": "FILE", + "url": f"files/{BUCKET}/", + } + result = process_block_response(FileMetadata, _make_response(payload)) - def test_file_metadata_full_real_payload(self): - """The full payload from the bug report must parse without error.""" - response = _make_response(REAL_API_PAYLOAD) - result = process_block_response(FileMetadata, response) assert result.name is None - assert result.bucket == "684f6Lz7ubje66aoCRsa5c" + assert result.bucket == BUCKET + assert result.node_type == "FOLDER" + assert result.parent_path is None + assert result.resource_type == "FILE" + assert result.url == f"files/{BUCKET}/" + assert result.items is not None and len(result.items) == 1 + item = result.items[0] + assert item.name == "ontologies.pdf" + assert item.bucket == BUCKET + assert item.node_type == "ITEM" + assert item.parent_path is None + assert item.resource_type == "FILE" + assert item.content_length == 2207949 + assert item.content_type == "application/pdf" + assert item.url == f"files/{BUCKET}/ontologies.pdf" From 3403b754ec6d1d3f8bf7284bf0cbeeba3a94369a Mon Sep 17 00:00:00 2001 From: Aleksei Korota Date: Thu, 26 Mar 2026 12:52:09 +0300 Subject: [PATCH 4/5] fix: simplify tests for FileMetadata by consolidating payload structure and verifying round-trip serialization --- tests/test_response_processing.py | 111 ++++++------------------------ 1 file changed, 20 insertions(+), 91 deletions(-) diff --git a/tests/test_response_processing.py b/tests/test_response_processing.py index 1ee5ca0..6b603dc 100644 --- a/tests/test_response_processing.py +++ b/tests/test_response_processing.py @@ -1,99 +1,28 @@ -import json - -import httpx - -from aidial_client._utils._response_processing import process_block_response from aidial_client.types.metadata import FileMetadata - -def _make_response(payload: dict, status_code: int = 200) -> httpx.Response: - raw = json.dumps(payload).encode() - return httpx.Response( - status_code=status_code, - headers={"content-type": "application/json"}, - content=raw, - ) - - BUCKET = "684f6Lz7ubje66aoCRsa5c" - -class TestFileMetadataName: - def test_name_is_none(self): - """Root folder returned by the API has name=null; must parse without error.""" - payload = { +PAYLOAD = { + "bucket": BUCKET, + "name": None, + "node_type": "FOLDER", + "parent_path": None, + "resource_type": "FILE", + "url": f"files/{BUCKET}/", + "items": [ + { "bucket": BUCKET, - "items": [ - { - "bucket": BUCKET, - "items": None, - "name": "appdata", - "nodeType": "FOLDER", - "parentPath": None, - "resourceType": "FILE", - "url": f"files/{BUCKET}/appdata/", - } - ], - "name": None, - "nodeType": "FOLDER", - "parentPath": None, - "resourceType": "FILE", - "url": f"files/{BUCKET}/", + "name": "appdata", + "node_type": "FOLDER", + "parent_path": None, + "resource_type": "FILE", + "url": f"files/{BUCKET}/appdata/", } - result = process_block_response(FileMetadata, _make_response(payload)) - - assert result.name is None - assert result.bucket == BUCKET - assert result.node_type == "FOLDER" - assert result.parent_path is None - assert result.resource_type == "FILE" - assert result.url == f"files/{BUCKET}/" - assert result.items is not None and len(result.items) == 1 - item = result.items[0] - assert item.name == "appdata" - assert item.bucket == BUCKET - assert item.node_type == "FOLDER" - assert item.parent_path is None - assert item.resource_type == "FILE" - assert item.url == f"files/{BUCKET}/appdata/" + ], +} - def test_name_is_string(self): - """Root folder whose single child is a file (non-null name).""" - payload = { - "bucket": BUCKET, - "items": [ - { - "bucket": BUCKET, - "contentLength": 2207949, - "contentType": "application/pdf", - "name": "ontologies.pdf", - "nodeType": "ITEM", - "parentPath": None, - "resourceType": "FILE", - "url": f"files/{BUCKET}/ontologies.pdf", - } - ], - "name": None, - "nodeType": "FOLDER", - "parentPath": None, - "resourceType": "FILE", - "url": f"files/{BUCKET}/", - } - result = process_block_response(FileMetadata, _make_response(payload)) - assert result.name is None - assert result.bucket == BUCKET - assert result.node_type == "FOLDER" - assert result.parent_path is None - assert result.resource_type == "FILE" - assert result.url == f"files/{BUCKET}/" - assert result.items is not None and len(result.items) == 1 - item = result.items[0] - assert item.name == "ontologies.pdf" - assert item.bucket == BUCKET - assert item.node_type == "ITEM" - assert item.parent_path is None - assert item.resource_type == "FILE" - assert item.content_length == 2207949 - assert item.content_type == "application/pdf" - assert item.url == f"files/{BUCKET}/ontologies.pdf" +def test_name_is_none_round_trip(): + """Root folder has name=None; parse→serialize must reproduce the original payload.""" + result = FileMetadata(**PAYLOAD) + assert result.model_dump(exclude_unset=True) == PAYLOAD From a4a2dbc2d31a7815a006bdcde5760be6c8233b98 Mon Sep 17 00:00:00 2001 From: Aleksei Korota Date: Thu, 26 Mar 2026 14:15:49 +0300 Subject: [PATCH 5/5] fix: update payload structure to use camelCase and enhance test for None name handling --- tests/test_response_processing.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_response_processing.py b/tests/test_response_processing.py index 6b603dc..c5ca642 100644 --- a/tests/test_response_processing.py +++ b/tests/test_response_processing.py @@ -5,17 +5,17 @@ PAYLOAD = { "bucket": BUCKET, "name": None, - "node_type": "FOLDER", - "parent_path": None, - "resource_type": "FILE", + "nodeType": "FOLDER", + "parentPath": None, + "resourceType": "FILE", "url": f"files/{BUCKET}/", "items": [ { "bucket": BUCKET, "name": "appdata", - "node_type": "FOLDER", - "parent_path": None, - "resource_type": "FILE", + "nodeType": "FOLDER", + "parentPath": None, + "resourceType": "FILE", "url": f"files/{BUCKET}/appdata/", } ], @@ -23,6 +23,8 @@ def test_name_is_none_round_trip(): - """Root folder has name=None; parse→serialize must reproduce the original payload.""" + """Root folder returned by the API has name=None; parse→serialize must reproduce the original payload.""" + assert PAYLOAD["name"] is None result = FileMetadata(**PAYLOAD) - assert result.model_dump(exclude_unset=True) == PAYLOAD + assert result.name is None + assert result.model_dump(by_alias=True, exclude_unset=True) == PAYLOAD