From c54568a8685968d8f9daec413dc0835c75b6ffc7 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Tue, 16 Dec 2025 12:15:38 +0530 Subject: [PATCH 01/17] feat:ElasticSearchAPI --- examples/file/search_files.py | 74 ++++++++++++++ nisystemlink/clients/file/_file_client.py | 17 ++++ nisystemlink/clients/file/models/__init__.py | 2 + .../file/models/_search_files_request.py | 32 ++++++ .../file/models/_search_files_response.py | 99 +++++++++++++++++++ tests/integration/file/test_file_client.py | 88 +++++++++++++++++ 6 files changed, 312 insertions(+) create mode 100644 examples/file/search_files.py create mode 100644 nisystemlink/clients/file/models/_search_files_request.py create mode 100644 nisystemlink/clients/file/models/_search_files_response.py diff --git a/examples/file/search_files.py b/examples/file/search_files.py new file mode 100644 index 00000000..a187d1a8 --- /dev/null +++ b/examples/file/search_files.py @@ -0,0 +1,74 @@ +"""Example demonstrating how to search for files using the File API.""" + +from nisystemlink.clients.core import HttpConfiguration +from nisystemlink.clients.file import FileClient, models + +# Configure connection to SystemLink server +server_configuration = HttpConfiguration( + server_uri="https://your-server-url.com", + api_key="YourAPIKeyGeneratedFromSystemLink", +) + +client = FileClient(configuration=server_configuration) + +# Example 1: Basic file search with filter +print("Example 1: Search files with filter") +search_request = models.SearchFilesRequest( + filter='name:("myfile.txt")', + skip=0, + take=10, +) + +response = client.search_files(search_request) +print(f"Found {response.total_count} file(s) matching the filter") +if response.available_files: + for file in response.available_files: + print(f" - {file.name} (ID: {file.id}, Size: {file.size} bytes)") + +# Example 2: Search with pagination and sorting +print("\nExample 2: Search with pagination and sorting") +search_request = models.SearchFilesRequest( + filter='size:([1000 TO *])', + skip=0, + take=20, + order_by="created", + order_by_descending=True, +) + +response = client.search_files(search_request) +print(f"Found {response.total_count} file(s) larger than 1000 bytes") +if response.available_files: + for file in response.available_files: + print( + f" - {file.name} created at {file.created} (Size: {file.size} bytes)" + ) + +# Example 3: Search files in a specific workspace +print("\nExample 3: Search files in a specific workspace") +search_request = models.SearchFilesRequest( + filter='workspace:("my-workspace-id")', + skip=0, + take=10, +) + +response = client.search_files(search_request) +print(f"Found {response.total_count} file(s) in the workspace") +if response.available_files: + for file in response.available_files: + print(f" - {file.name} in workspace {file.workspace}") + +# Example 4: Search by content type +print("\nExample 4: Search by content type") +search_request = models.SearchFilesRequest( + filter='contentType:("application/json")', + skip=0, + take=10, + order_by="name", + order_by_descending=False, +) + +response = client.search_files(search_request) +print(f"Found {response.total_count} JSON file(s)") +if response.available_files: + for file in response.available_files: + print(f" - {file.name} ({file.content_type})") diff --git a/nisystemlink/clients/file/_file_client.py b/nisystemlink/clients/file/_file_client.py index 052091fd..35c6f64d 100644 --- a/nisystemlink/clients/file/_file_client.py +++ b/nisystemlink/clients/file/_file_client.py @@ -163,6 +163,23 @@ def query_files_linq( """ ... + @post("service-groups/Default/search-files") + def search_files( + self, request: models.SearchFilesRequest + ) -> models.SearchFilesResponse: + """Search for files based on filter criteria. + + Args: + request: The search request containing filter, pagination, and sorting parameters. + + Returns: + SearchFilesResponse: Response containing matching files and total count. + + Raises: + ApiException: if unable to communicate with the File Service. + """ + ... + @params({"force": True}) # type: ignore @delete("service-groups/Default/files/{id}", args=[Path]) def delete_file(self, id: str) -> None: diff --git a/nisystemlink/clients/file/models/__init__.py b/nisystemlink/clients/file/models/__init__.py index 4d1dede8..47f92355 100644 --- a/nisystemlink/clients/file/models/__init__.py +++ b/nisystemlink/clients/file/models/__init__.py @@ -5,5 +5,7 @@ from ._operations import V1Operations from ._update_metadata import UpdateMetadataRequest from ._file_linq_query import FileLinqQueryRequest, FileLinqQueryResponse +from ._search_files_request import SearchFilesRequest +from ._search_files_response import SearchFilesResponse, SearchFileMetadata # flake8: noqa diff --git a/nisystemlink/clients/file/models/_search_files_request.py b/nisystemlink/clients/file/models/_search_files_request.py new file mode 100644 index 00000000..2239ee4c --- /dev/null +++ b/nisystemlink/clients/file/models/_search_files_request.py @@ -0,0 +1,32 @@ +from typing import Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + + +class SearchFilesRequest(JsonModel): + """Request model for searching files.""" + + filter: Optional[str] = None + """ + Filter string for searching files. + """ + + skip: Optional[int] = None + """ + How many files to skip in the result when paging. + """ + + take: Optional[int] = None + """ + How many files to return in the result. + """ + + order_by: Optional[str] = None + """ + The name of the metadata field to sort by. + """ + + order_by_descending: Optional[bool] = None + """ + Whether to sort in descending order. + """ diff --git a/nisystemlink/clients/file/models/_search_files_response.py b/nisystemlink/clients/file/models/_search_files_response.py new file mode 100644 index 00000000..3ca1e068 --- /dev/null +++ b/nisystemlink/clients/file/models/_search_files_response.py @@ -0,0 +1,99 @@ +from datetime import datetime +from typing import Dict, List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + +from ._file_linq_query import TotalCount + + +class SearchFileMetadata(JsonModel): + """File metadata returned from search files operation.""" + + id: Optional[str] = None + """ + The file's unique identifier. + """ + + user_id: Optional[str] = None + """ + The ID of the user who created the file. + """ + + org_id: Optional[str] = None + """ + The organization ID that owns the file. + """ + + service_group: Optional[str] = None + """ + The service group that owns the file. + """ + + name: Optional[str] = None + """ + The name of the file. + """ + + created: Optional[datetime] = None + """ + The date and time the file was created. + """ + + size: Optional[int] = None + """ + The file size in bytes. + """ + + content_type: Optional[str] = None + """ + The content type of the file. + """ + + properties: Optional[Dict[str, str]] = None + """ + The file's properties as key-value pairs. + """ + + updated: Optional[datetime] = None + """ + The date and time the file was last updated. + """ + + deleted: Optional[bool] = None + """ + Whether the file has been deleted. + """ + + encryption_schema: Optional[int] = None + """ + The encryption schema used for the file. + """ + + download_key: Optional[str] = None + """ + The key used to download the file. + """ + + chunks: Optional[int] = None + """ + The number of chunks in the file. + """ + + workspace: Optional[str] = None + """ + The workspace the file belongs to. + """ + + +class SearchFilesResponse(JsonModel): + """Response model for search files operation.""" + + available_files: Optional[List[SearchFileMetadata]] = None + """ + List of files matching the search criteria. + """ + + total_count: Optional[TotalCount] = None + """ + Total number of files matching the search criteria. + """ diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index 4ae5d7e5..a1f5a7c2 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -6,12 +6,14 @@ from random import choices, randint from typing import BinaryIO +import backoff # type: ignore import pytest # type: ignore from nisystemlink.clients.core import ApiException from nisystemlink.clients.file import FileClient from nisystemlink.clients.file.models import ( FileLinqQueryOrderBy, FileLinqQueryRequest, + SearchFilesRequest, UpdateMetadataRequest, ) from nisystemlink.clients.file.utilities import rename_file @@ -263,3 +265,89 @@ def test__query_files_linq__filter_returns_no_results(self, client: FileClient): assert response.total_count is not None assert response.total_count.value == 0 assert response.total_count.relation == "eq" + + @backoff.on_exception( + backoff.expo, + AssertionError, + max_tries=5, + max_time=5, + ) + def test__search_files__succeeds( + self, client: FileClient, test_file, random_filename_extension: str + ): + """Test search_files with filtering, pagination, and ordering.""" + # Upload 5 files to test various scenarios + NUM_FILES = 5 + file_ids = [] + file_prefix = f"{PREFIX}search_test_{randint(1000, 9999)}" + + for i in range(NUM_FILES): + file_name = f"{file_prefix}_{i}.bin" + file_id = test_file(file_name=file_name) + file_ids.append(file_id) + + # Search with filter (by name pattern), pagination, and ordering + search_request = SearchFilesRequest( + filter=f'(name:("{file_prefix}*"))', + skip=1, + take=3, + order_by="name", + order_by_descending=True, + ) + response = client.search_files(request=search_request) + + assert response.available_files is not None + assert response.total_count is not None + assert response.total_count.value == 5 + assert response.total_count.relation is not None + assert len(response.available_files) == 3 # skip=1, take=3 + + # Verify all fields in response + for file_metadata in response.available_files: + assert file_metadata.id in file_ids + assert file_metadata.name is not None + assert file_metadata.name.startswith(file_prefix) + assert file_metadata.created is not None + assert isinstance(file_metadata.created, datetime) + assert file_metadata.updated is not None + assert isinstance(file_metadata.updated, datetime) + assert file_metadata.workspace is not None + assert file_metadata.size is not None + assert file_metadata.properties is not None + assert "Name" in file_metadata.properties + + # Verify descending order by name + returned_names = [f.name for f in response.available_files] + assert returned_names == sorted(returned_names, reverse=True) + + def test__search_files__no_filter_succeeds(self, client: FileClient, test_file): + file_id = test_file() + + search_request = SearchFilesRequest(skip=0, take=10) + response = client.search_files(request=search_request) + + assert response.available_files is not None + assert response.total_count is not None + assert response.total_count.value >= 1 + assert len(response.available_files) >= 1 + + def test__search_files__invalid_filter_raises(self, client: FileClient): + search_request = SearchFilesRequest(filter="invalid filter syntax") + + with pytest.raises(ApiException): + client.search_files(request=search_request) + + def test__search_files__filter_returns_no_results(self, client: FileClient): + unique_nonexistent_name = ( + f"{PREFIX}nonexistent_search_file_{randint(100000, 999999)}.random_ext" + ) + + search_request = SearchFilesRequest( + filter=f'(name:("{unique_nonexistent_name}"))', skip=0, take=10 + ) + response = client.search_files(request=search_request) + + assert response.available_files is not None + assert len(response.available_files) == 0 + assert response.total_count is not None + assert response.total_count.value == 0 From b8181de6bb83485b77750b2142f170e13ad5ee7a Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Wed, 17 Dec 2025 16:44:25 +0530 Subject: [PATCH 02/17] fix:BackOffLogic --- .../clients/file/models/_file_metadata.py | 15 ++-- .../file/models/_search_files_request.py | 12 ++- .../file/models/_search_files_response.py | 57 ++++--------- tests/integration/file/test_file_client.py | 81 ++++++++++--------- 4 files changed, 71 insertions(+), 94 deletions(-) diff --git a/nisystemlink/clients/file/models/_file_metadata.py b/nisystemlink/clients/file/models/_file_metadata.py index 680572a6..2f3e56c4 100644 --- a/nisystemlink/clients/file/models/_file_metadata.py +++ b/nisystemlink/clients/file/models/_file_metadata.py @@ -41,11 +41,6 @@ class BaseFileMetadata(JsonModel): this value is -1 and the size64 parameter contains the correct value. """ - size64: int | None = None - """ - The 64-bit file size in bytes - """ - workspace: str | None = None """ The workspace the file belongs to @@ -54,6 +49,11 @@ class BaseFileMetadata(JsonModel): class FileMetadata(BaseFileMetadata): + size64: int | None = None + """ + The 64-bit file size in bytes + """ + field_links: Dict[str, Link] | None = Field(None, alias="_links") """ The links to access and manipulate the file: @@ -88,6 +88,11 @@ class FileMetadata(BaseFileMetadata): class LinqQueryFileMetadata(BaseFileMetadata): """Metadata for a file returned by a LINQ query.""" + size64: int | None = None + """ + The 64-bit file size in bytes + """ + updated: datetime | None = None """ The date and time the file was last updated in the file service. diff --git a/nisystemlink/clients/file/models/_search_files_request.py b/nisystemlink/clients/file/models/_search_files_request.py index 2239ee4c..1cf39a19 100644 --- a/nisystemlink/clients/file/models/_search_files_request.py +++ b/nisystemlink/clients/file/models/_search_files_request.py @@ -1,32 +1,30 @@ -from typing import Optional - from nisystemlink.clients.core._uplink._json_model import JsonModel class SearchFilesRequest(JsonModel): """Request model for searching files.""" - filter: Optional[str] = None + filter: str | None = None """ Filter string for searching files. """ - skip: Optional[int] = None + skip: int | None = None """ How many files to skip in the result when paging. """ - take: Optional[int] = None + take: int | None = None """ How many files to return in the result. """ - order_by: Optional[str] = None + order_by: str | None = None """ The name of the metadata field to sort by. """ - order_by_descending: Optional[bool] = None + order_by_descending: bool | None = None """ Whether to sort in descending order. """ diff --git a/nisystemlink/clients/file/models/_search_files_response.py b/nisystemlink/clients/file/models/_search_files_response.py index 3ca1e068..8cae3cf5 100644 --- a/nisystemlink/clients/file/models/_search_files_response.py +++ b/nisystemlink/clients/file/models/_search_files_response.py @@ -1,99 +1,70 @@ from datetime import datetime -from typing import Dict, List, Optional +from typing import List from nisystemlink.clients.core._uplink._json_model import JsonModel from ._file_linq_query import TotalCount +from ._file_metadata import BaseFileMetadata -class SearchFileMetadata(JsonModel): +class SearchFileMetadata(BaseFileMetadata): """File metadata returned from search files operation.""" - id: Optional[str] = None - """ - The file's unique identifier. - """ - - user_id: Optional[str] = None + user_id: str | None = None """ The ID of the user who created the file. """ - org_id: Optional[str] = None + org_id: str | None = None """ The organization ID that owns the file. """ - service_group: Optional[str] = None - """ - The service group that owns the file. - """ - - name: Optional[str] = None + name: str | None = None """ The name of the file. """ - created: Optional[datetime] = None - """ - The date and time the file was created. - """ - - size: Optional[int] = None - """ - The file size in bytes. - """ - - content_type: Optional[str] = None + content_type: str | None = None """ The content type of the file. """ - properties: Optional[Dict[str, str]] = None - """ - The file's properties as key-value pairs. - """ - - updated: Optional[datetime] = None + updated: datetime | None = None """ The date and time the file was last updated. """ - deleted: Optional[bool] = None + deleted: bool | None = None """ Whether the file has been deleted. """ - encryption_schema: Optional[int] = None + encryption_schema: int | None = None """ The encryption schema used for the file. """ - download_key: Optional[str] = None + download_key: str | None = None """ The key used to download the file. """ - chunks: Optional[int] = None + chunks: int | None = None """ The number of chunks in the file. """ - workspace: Optional[str] = None - """ - The workspace the file belongs to. - """ - class SearchFilesResponse(JsonModel): """Response model for search files operation.""" - available_files: Optional[List[SearchFileMetadata]] = None + available_files: List[SearchFileMetadata] | None = None """ List of files matching the search criteria. """ - total_count: Optional[TotalCount] = None + total_count: TotalCount | None = None """ Total number of files matching the search criteria. """ diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index a1f5a7c2..05ca0619 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -266,12 +266,6 @@ def test__query_files_linq__filter_returns_no_results(self, client: FileClient): assert response.total_count.value == 0 assert response.total_count.relation == "eq" - @backoff.on_exception( - backoff.expo, - AssertionError, - max_tries=5, - max_time=5, - ) def test__search_files__succeeds( self, client: FileClient, test_file, random_filename_extension: str ): @@ -279,46 +273,55 @@ def test__search_files__succeeds( # Upload 5 files to test various scenarios NUM_FILES = 5 file_ids = [] - file_prefix = f"{PREFIX}search_test_{randint(1000, 9999)}" for i in range(NUM_FILES): - file_name = f"{file_prefix}_{i}.bin" + file_name = f"{PREFIX}_{i}.bin" file_id = test_file(file_name=file_name) file_ids.append(file_id) - # Search with filter (by name pattern), pagination, and ordering - search_request = SearchFilesRequest( - filter=f'(name:("{file_prefix}*"))', - skip=1, - take=3, - order_by="name", - order_by_descending=True, + # Search with retry logic + @backoff.on_exception( + backoff.expo, + AssertionError, + max_tries=5, + max_time=10, ) - response = client.search_files(request=search_request) - - assert response.available_files is not None - assert response.total_count is not None - assert response.total_count.value == 5 - assert response.total_count.relation is not None - assert len(response.available_files) == 3 # skip=1, take=3 - - # Verify all fields in response - for file_metadata in response.available_files: - assert file_metadata.id in file_ids - assert file_metadata.name is not None - assert file_metadata.name.startswith(file_prefix) - assert file_metadata.created is not None - assert isinstance(file_metadata.created, datetime) - assert file_metadata.updated is not None - assert isinstance(file_metadata.updated, datetime) - assert file_metadata.workspace is not None - assert file_metadata.size is not None - assert file_metadata.properties is not None - assert "Name" in file_metadata.properties + def search_and_verify(): + # Search with filter (by name pattern), pagination, and ordering + search_request = SearchFilesRequest( + filter=f'(name:("{PREFIX}*"))', + skip=1, + take=3, + order_by="name", + order_by_descending=True, + ) + response = client.search_files(request=search_request) + + assert response.available_files is not None + assert response.total_count is not None + assert response.total_count.value == 3 + assert response.total_count.relation is not None + assert len(response.available_files) == 3 # skip=1, take=3 + + # Verify all fields in response + for file_metadata in response.available_files: + assert file_metadata.id in file_ids + assert file_metadata.properties.get("Name") is not None + assert file_metadata.properties.get("Name").startswith(PREFIX) + assert file_metadata.created is not None + assert isinstance(file_metadata.created, datetime) + assert file_metadata.updated is not None + assert isinstance(file_metadata.updated, datetime) + assert file_metadata.workspace is not None + assert file_metadata.size is not None + assert file_metadata.properties is not None + assert "Name" in file_metadata.properties + + # Verify descending order by name + returned_names = [f.properties.get("Name") for f in response.available_files] + assert returned_names == sorted(returned_names, reverse=True) - # Verify descending order by name - returned_names = [f.name for f in response.available_files] - assert returned_names == sorted(returned_names, reverse=True) + search_and_verify() def test__search_files__no_filter_succeeds(self, client: FileClient, test_file): file_id = test_file() From 7c750cce68fc37042d0b2b73800b99e059466013 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Wed, 17 Dec 2025 17:04:57 +0530 Subject: [PATCH 03/17] fix:Example --- examples/file/search_files.py | 84 +++++++++++++--------- tests/integration/file/test_file_client.py | 27 ++++--- 2 files changed, 67 insertions(+), 44 deletions(-) diff --git a/examples/file/search_files.py b/examples/file/search_files.py index a187d1a8..19a78aff 100644 --- a/examples/file/search_files.py +++ b/examples/file/search_files.py @@ -1,34 +1,57 @@ """Example demonstrating how to search for files using the File API.""" +import io +import time + from nisystemlink.clients.core import HttpConfiguration from nisystemlink.clients.file import FileClient, models # Configure connection to SystemLink server server_configuration = HttpConfiguration( - server_uri="https://your-server-url.com", + server_uri="https://yourserver.yourcompany.com", api_key="YourAPIKeyGeneratedFromSystemLink", ) client = FileClient(configuration=server_configuration) -# Example 1: Basic file search with filter -print("Example 1: Search files with filter") +# Upload a test file first +print("Uploading test file...") +test_file_content = b"This is a test file for search demonstration." +test_file = io.BytesIO(test_file_content) +test_file.name = "search-example-test-file.txt" + +file_id = client.upload_file(file=test_file) +print(f"Uploaded test file with ID: {file_id}") + +# Wait for the file to be indexed for search +# Note: Files may take a few seconds to appear in search results after upload +print("Waiting 5 seconds for file to be indexed for search...") +time.sleep(5) +print() + +# Example 1: Basic file search with filter - search for the uploaded file +print("Example 1: Search for the uploaded test file") search_request = models.SearchFilesRequest( - filter='name:("myfile.txt")', + filter='name:("search-example-test-file.txt")', skip=0, take=10, ) response = client.search_files(search_request) -print(f"Found {response.total_count} file(s) matching the filter") +print( + f"Found {response.total_count.value if response.total_count else 0} file(s) matching the filter" +) if response.available_files: for file in response.available_files: - print(f" - {file.name} (ID: {file.id}, Size: {file.size} bytes)") + if file.properties: + print( + f"- {file.properties.get('Name')} (ID: {file.id}, Size: {file.size} bytes)" + ) -# Example 2: Search with pagination and sorting -print("\nExample 2: Search with pagination and sorting") +# Example 2: Search with wildcard pattern +print("\nExample 2: Search with wildcard pattern") search_request = models.SearchFilesRequest( - filter='size:([1000 TO *])', + filter='name:("search-example*")', skip=0, take=20, order_by="created", @@ -36,39 +59,34 @@ ) response = client.search_files(search_request) -print(f"Found {response.total_count} file(s) larger than 1000 bytes") -if response.available_files: - for file in response.available_files: - print( - f" - {file.name} created at {file.created} (Size: {file.size} bytes)" - ) - -# Example 3: Search files in a specific workspace -print("\nExample 3: Search files in a specific workspace") -search_request = models.SearchFilesRequest( - filter='workspace:("my-workspace-id")', - skip=0, - take=10, +print( + f"Found {response.total_count.value if response.total_count else 0} file(s) starting with 'search-example'" ) - -response = client.search_files(search_request) -print(f"Found {response.total_count} file(s) in the workspace") if response.available_files: for file in response.available_files: - print(f" - {file.name} in workspace {file.workspace}") + if file.properties: + print( + f"- {file.properties.get('Name')} created at {file.created} (Size: {file.size} bytes)" + ) -# Example 4: Search by content type -print("\nExample 4: Search by content type") +# Example 3: Search by size range +print("\nExample 3: Search by size range") search_request = models.SearchFilesRequest( - filter='contentType:("application/json")', + filter="size:([1 TO 1000])", skip=0, take=10, - order_by="name", - order_by_descending=False, ) response = client.search_files(search_request) -print(f"Found {response.total_count} JSON file(s)") +print( + f"Found {response.total_count.value if response.total_count else 0} file(s) between 1 and 1000 bytes" +) if response.available_files: for file in response.available_files: - print(f" - {file.name} ({file.content_type})") + if file.properties: + print(f"- {file.properties.get('Name')} (Size: {file.size} bytes)") + +# Clean up: delete the test file +print("\nCleaning up...") +client.delete_file(id=file_id) +print(f"Deleted test file with ID: {file_id}") diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index 05ca0619..f72e93ff 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -273,7 +273,7 @@ def test__search_files__succeeds( # Upload 5 files to test various scenarios NUM_FILES = 5 file_ids = [] - + for i in range(NUM_FILES): file_name = f"{PREFIX}_{i}.bin" file_id = test_file(file_name=file_name) @@ -286,28 +286,29 @@ def test__search_files__succeeds( max_tries=5, max_time=10, ) - def search_and_verify(): + def search_and_verify() -> None: # Search with filter (by name pattern), pagination, and ordering search_request = SearchFilesRequest( - filter=f'(name:("{PREFIX}*"))', + filter=f'(name: ("{PREFIX}*"))', skip=1, take=3, order_by="name", order_by_descending=True, ) response = client.search_files(request=search_request) - + assert response.available_files is not None assert response.total_count is not None assert response.total_count.value == 3 assert response.total_count.relation is not None assert len(response.available_files) == 3 # skip=1, take=3 - + # Verify all fields in response for file_metadata in response.available_files: assert file_metadata.id in file_ids + assert file_metadata.properties is not None assert file_metadata.properties.get("Name") is not None - assert file_metadata.properties.get("Name").startswith(PREFIX) + assert file_metadata.properties.get("Name", "").startswith(PREFIX) assert file_metadata.created is not None assert isinstance(file_metadata.created, datetime) assert file_metadata.updated is not None @@ -316,15 +317,19 @@ def search_and_verify(): assert file_metadata.size is not None assert file_metadata.properties is not None assert "Name" in file_metadata.properties - + # Verify descending order by name - returned_names = [f.properties.get("Name") for f in response.available_files] + returned_names = [ + f.properties.get("Name", "") + for f in response.available_files + if f.properties + ] assert returned_names == sorted(returned_names, reverse=True) - + search_and_verify() def test__search_files__no_filter_succeeds(self, client: FileClient, test_file): - file_id = test_file() + test_file() search_request = SearchFilesRequest(skip=0, take=10) response = client.search_files(request=search_request) @@ -346,7 +351,7 @@ def test__search_files__filter_returns_no_results(self, client: FileClient): ) search_request = SearchFilesRequest( - filter=f'(name:("{unique_nonexistent_name}"))', skip=0, take=10 + filter=f'(name: ("{unique_nonexistent_name}"))', skip=0, take=10 ) response = client.search_files(request=search_request) From 815d50d0f4b18c3451f34777784d2d774fb93add Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Wed, 17 Dec 2025 17:59:43 +0530 Subject: [PATCH 04/17] fix:AddBackoffDependency --- poetry.lock | 14 +++++++++++++- pyproject.toml | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 4d51dd04..b0b6309b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -45,6 +45,18 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.31.0)"] +[[package]] +name = "backoff" +version = "2.2.1" +description = "Function decoration for backoff and retry" +optional = false +python-versions = ">=3.7,<4.0" +groups = ["dev"] +files = [ + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, +] + [[package]] name = "black" version = "24.10.0" @@ -1553,4 +1565,4 @@ pyarrow = ["pyarrow"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "bf33275736a4381c4bb2bca4394b83ddcc4bfd3aa37edb927cdec587c0c46cc5" +content-hash = "d22edb04521c040d6362689f7118d092f97f1d3d2e21232b92fdad71b35f0f63" diff --git a/pyproject.toml b/pyproject.toml index 3b90fddc..027257ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ pyarrow = { version = "^21.0.0", optional = true } pyarrow = ["pyarrow"] [tool.poetry.group.dev.dependencies] +backoff = "^2.2.1" black = ">=22.10,<25.0" flake8 = "^7.3.0" flake8-import-order = "^0.19.2" From 9180d876f23efe2f07f6cfa211a49e085a5b2571 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Wed, 17 Dec 2025 18:09:05 +0530 Subject: [PATCH 05/17] fix:SearchTestPrefix --- tests/integration/file/test_file_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index f72e93ff..e5d5b112 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -273,9 +273,10 @@ def test__search_files__succeeds( # Upload 5 files to test various scenarios NUM_FILES = 5 file_ids = [] + file_prefix = f"{PREFIX}search_test_" for i in range(NUM_FILES): - file_name = f"{PREFIX}_{i}.bin" + file_name = f"{file_prefix}_{i}.bin" file_id = test_file(file_name=file_name) file_ids.append(file_id) @@ -289,7 +290,7 @@ def test__search_files__succeeds( def search_and_verify() -> None: # Search with filter (by name pattern), pagination, and ordering search_request = SearchFilesRequest( - filter=f'(name: ("{PREFIX}*"))', + filter=f'(name: ("{file_prefix}*"))', skip=1, take=3, order_by="name", @@ -308,7 +309,7 @@ def search_and_verify() -> None: assert file_metadata.id in file_ids assert file_metadata.properties is not None assert file_metadata.properties.get("Name") is not None - assert file_metadata.properties.get("Name", "").startswith(PREFIX) + assert file_metadata.properties.get("Name", "").startswith(file_prefix) assert file_metadata.created is not None assert isinstance(file_metadata.created, datetime) assert file_metadata.updated is not None From 43c10519e96475b9eda9e72c66e0a8e50b4c3d52 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Thu, 18 Dec 2025 13:57:08 +0530 Subject: [PATCH 06/17] fix:TestChanges --- nisystemlink/clients/file/models/__init__.py | 5 +- .../file/models/_base_file_response.py | 35 ++++++++++ .../clients/file/models/_file_linq_query.py | 32 ++------- .../clients/file/models/_file_metadata.py | 19 ++--- .../file/models/_search_files_response.py | 69 +------------------ tests/integration/file/test_file_client.py | 37 +++++----- 6 files changed, 73 insertions(+), 124 deletions(-) create mode 100644 nisystemlink/clients/file/models/_base_file_response.py diff --git a/nisystemlink/clients/file/models/__init__.py b/nisystemlink/clients/file/models/__init__.py index 47f92355..baf28272 100644 --- a/nisystemlink/clients/file/models/__init__.py +++ b/nisystemlink/clients/file/models/__init__.py @@ -1,4 +1,4 @@ -from ._file_metadata import FileMetadata +from ._file_metadata import FileMetadata, FileQueryMetadata from ._file_query_order_by import FileQueryOrderBy, FileLinqQueryOrderBy from ._file_query_response import FileQueryResponse from ._link import Link @@ -6,6 +6,7 @@ from ._update_metadata import UpdateMetadataRequest from ._file_linq_query import FileLinqQueryRequest, FileLinqQueryResponse from ._search_files_request import SearchFilesRequest -from ._search_files_response import SearchFilesResponse, SearchFileMetadata +from ._search_files_response import SearchFilesResponse +from ._base_file_response import BaseFileResponse, TotalCount # flake8: noqa diff --git a/nisystemlink/clients/file/models/_base_file_response.py b/nisystemlink/clients/file/models/_base_file_response.py new file mode 100644 index 00000000..c3e0eda4 --- /dev/null +++ b/nisystemlink/clients/file/models/_base_file_response.py @@ -0,0 +1,35 @@ +from typing import List + +from nisystemlink.clients.core._uplink._json_model import JsonModel + +from ._file_metadata import FileQueryMetadata + + +class TotalCount(JsonModel): + """The total number of files that match the query regardless of skip and take values""" + + relation: str + """ + Describes the relation the returned total count value has with respect to the total number of + files matched by the query. + + Possible values: + + - "eq" -> equals, meaning that the returned items are all the items that matched the filter. + + - "gte" -> greater or equal, meaning that there the take limit has been hit, but there are further + items that match the query in the database. + """ + + value: int + """Describes the number of files that were returned as a result of the query in the database""" + + +class BaseFileResponse(JsonModel): + """Base class for file response models containing a list of files and total count.""" + + available_files: List[FileQueryMetadata] + """The list of files returned by the query""" + + total_count: TotalCount + """The total number of files that match the query regardless of skip and take values""" diff --git a/nisystemlink/clients/file/models/_file_linq_query.py b/nisystemlink/clients/file/models/_file_linq_query.py index 8874fa6b..11eb00d1 100644 --- a/nisystemlink/clients/file/models/_file_linq_query.py +++ b/nisystemlink/clients/file/models/_file_linq_query.py @@ -1,7 +1,5 @@ -from typing import List - from nisystemlink.clients.core._uplink._json_model import JsonModel -from nisystemlink.clients.file.models._file_metadata import LinqQueryFileMetadata +from nisystemlink.clients.file.models._base_file_response import BaseFileResponse from nisystemlink.clients.file.models._file_query_order_by import FileLinqQueryOrderBy @@ -25,29 +23,7 @@ class FileLinqQueryRequest(JsonModel): """The maximum number of files to return in the response. Default value is 1000""" -class TotalCount(JsonModel): - """The total number of files that match the query regardless of skip and take values""" - - relation: str - """ - Describes the relation the returned total count value has with respect to the total number of - files matched by the query. - - Possible values: - - - "eq" -> equals, meaning that the returned items are all the items that matched the filter. - - - "gte" -> greater or equal, meaning that there the take limit has been hit, but there are further - items that match the query in the database. - """ - - value: int - """Describes the number of files that were returned as a result of the query in the database""" - - -class FileLinqQueryResponse(JsonModel): - available_files: List[LinqQueryFileMetadata] - """The list of files returned by the query""" +class FileLinqQueryResponse(BaseFileResponse): + """Response model for LINQ query operations.""" - total_count: TotalCount - """The total number of files that match the query regardless of skip and take values""" + pass diff --git a/nisystemlink/clients/file/models/_file_metadata.py b/nisystemlink/clients/file/models/_file_metadata.py index 2f3e56c4..9a432567 100644 --- a/nisystemlink/clients/file/models/_file_metadata.py +++ b/nisystemlink/clients/file/models/_file_metadata.py @@ -41,6 +41,11 @@ class BaseFileMetadata(JsonModel): this value is -1 and the size64 parameter contains the correct value. """ + size64: int | None = None + """ + The 64-bit file size in bytes + """ + workspace: str | None = None """ The workspace the file belongs to @@ -49,11 +54,6 @@ class BaseFileMetadata(JsonModel): class FileMetadata(BaseFileMetadata): - size64: int | None = None - """ - The 64-bit file size in bytes - """ - field_links: Dict[str, Link] | None = Field(None, alias="_links") """ The links to access and manipulate the file: @@ -85,13 +85,8 @@ class FileMetadata(BaseFileMetadata): """ -class LinqQueryFileMetadata(BaseFileMetadata): - """Metadata for a file returned by a LINQ query.""" - - size64: int | None = None - """ - The 64-bit file size in bytes - """ +class FileQueryMetadata(BaseFileMetadata): + """Metadata for a file returned by a query or search operation.""" updated: datetime | None = None """ diff --git a/nisystemlink/clients/file/models/_search_files_response.py b/nisystemlink/clients/file/models/_search_files_response.py index 8cae3cf5..33ea9c32 100644 --- a/nisystemlink/clients/file/models/_search_files_response.py +++ b/nisystemlink/clients/file/models/_search_files_response.py @@ -1,70 +1,7 @@ -from datetime import datetime -from typing import List +from ._base_file_response import BaseFileResponse -from nisystemlink.clients.core._uplink._json_model import JsonModel -from ._file_linq_query import TotalCount -from ._file_metadata import BaseFileMetadata - - -class SearchFileMetadata(BaseFileMetadata): - """File metadata returned from search files operation.""" - - user_id: str | None = None - """ - The ID of the user who created the file. - """ - - org_id: str | None = None - """ - The organization ID that owns the file. - """ - - name: str | None = None - """ - The name of the file. - """ - - content_type: str | None = None - """ - The content type of the file. - """ - - updated: datetime | None = None - """ - The date and time the file was last updated. - """ - - deleted: bool | None = None - """ - Whether the file has been deleted. - """ - - encryption_schema: int | None = None - """ - The encryption schema used for the file. - """ - - download_key: str | None = None - """ - The key used to download the file. - """ - - chunks: int | None = None - """ - The number of chunks in the file. - """ - - -class SearchFilesResponse(JsonModel): +class SearchFilesResponse(BaseFileResponse): """Response model for search files operation.""" - available_files: List[SearchFileMetadata] | None = None - """ - List of files matching the search criteria. - """ - - total_count: TotalCount | None = None - """ - Total number of files matching the search criteria. - """ + pass diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index e5d5b112..2d278c05 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -280,6 +280,14 @@ def test__search_files__succeeds( file_id = test_file(file_name=file_name) file_ids.append(file_id) + # Search with filter (by name pattern), pagination, and ordering + search_request = SearchFilesRequest( + filter=f'(name: ("{file_prefix}*"))', + skip=1, + take=3, + order_by="name", + ) + # Search with retry logic @backoff.on_exception( backoff.expo, @@ -288,14 +296,6 @@ def test__search_files__succeeds( max_time=10, ) def search_and_verify() -> None: - # Search with filter (by name pattern), pagination, and ordering - search_request = SearchFilesRequest( - filter=f'(name: ("{file_prefix}*"))', - skip=1, - take=3, - order_by="name", - order_by_descending=True, - ) response = client.search_files(request=search_request) assert response.available_files is not None @@ -320,14 +320,19 @@ def search_and_verify() -> None: assert "Name" in file_metadata.properties # Verify descending order by name - returned_names = [ - f.properties.get("Name", "") - for f in response.available_files - if f.properties - ] - assert returned_names == sorted(returned_names, reverse=True) - - search_and_verify() + # returned_names = [ + # f.properties.get("Name", "") + # for f in response.available_files + # if f.properties + # ] + # assert returned_names == sorted(returned_names, reverse=True) + + try: + search_and_verify() + except ApiException as exception: + raise Exception( + f"Request body: {search_request.model_dump()}" + ) from exception def test__search_files__no_filter_succeeds(self, client: FileClient, test_file): test_file() From 86c733d659a954d7652bf1637616acfc2f616057 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Thu, 18 Dec 2025 14:02:53 +0530 Subject: [PATCH 07/17] fix:Test Internal Server Error --- nisystemlink/clients/file/models/_search_files_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nisystemlink/clients/file/models/_search_files_request.py b/nisystemlink/clients/file/models/_search_files_request.py index 1cf39a19..89e01076 100644 --- a/nisystemlink/clients/file/models/_search_files_request.py +++ b/nisystemlink/clients/file/models/_search_files_request.py @@ -24,7 +24,7 @@ class SearchFilesRequest(JsonModel): The name of the metadata field to sort by. """ - order_by_descending: bool | None = None + order_by_descending: bool | None = False """ Whether to sort in descending order. """ From cb290b3568882c1c0d05c61bca299ea292458603 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Thu, 18 Dec 2025 15:17:35 +0530 Subject: [PATCH 08/17] fix:Search-API error --- tests/integration/file/test_file_client.py | 24 ++++++++-------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index 2d278c05..86af7092 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -286,13 +286,14 @@ def test__search_files__succeeds( skip=1, take=3, order_by="name", + order_by_descending=True, ) # Search with retry logic @backoff.on_exception( backoff.expo, - AssertionError, - max_tries=5, + (AssertionError, ApiException), + max_tries=3, max_time=10, ) def search_and_verify() -> None: @@ -320,19 +321,12 @@ def search_and_verify() -> None: assert "Name" in file_metadata.properties # Verify descending order by name - # returned_names = [ - # f.properties.get("Name", "") - # for f in response.available_files - # if f.properties - # ] - # assert returned_names == sorted(returned_names, reverse=True) - - try: - search_and_verify() - except ApiException as exception: - raise Exception( - f"Request body: {search_request.model_dump()}" - ) from exception + returned_names = [ + f.properties.get("Name", "") + for f in response.available_files + if f.properties + ] + assert returned_names == sorted(returned_names, reverse=True) def test__search_files__no_filter_succeeds(self, client: FileClient, test_file): test_file() From 68c52db691b6648131f65d154bb89a4c9f801d83 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Thu, 18 Dec 2025 15:46:58 +0530 Subject: [PATCH 09/17] feat:AddSkipForLinqQueryFilesRequest --- .../clients/file/models/_file_linq_query.py | 5 +++ tests/integration/file/test_file_client.py | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/nisystemlink/clients/file/models/_file_linq_query.py b/nisystemlink/clients/file/models/_file_linq_query.py index 11eb00d1..3c73c4db 100644 --- a/nisystemlink/clients/file/models/_file_linq_query.py +++ b/nisystemlink/clients/file/models/_file_linq_query.py @@ -13,6 +13,11 @@ class FileLinqQueryRequest(JsonModel): Example Filter syntax: '[property name][operator][operand] and [property name][operator][operand]' """ + skip: int | None = None + """ + How many files to skip in the result when paging. + """ + order_by: FileLinqQueryOrderBy | None = None """The property by which to order the files in the response.""" diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index 86af7092..076621e3 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -266,6 +266,40 @@ def test__query_files_linq__filter_returns_no_results(self, client: FileClient): assert response.total_count.value == 0 assert response.total_count.relation == "eq" + def test__query_files_linq__skip_and_take_pagination( + self, client: FileClient, test_file + ): + """Test query_files_linq with skip and take for pagination.""" + # Upload 5 files to test pagination + NUM_FILES = 5 + file_ids = [] + file_prefix = f"{PREFIX}pagination_test_" + + for i in range(NUM_FILES): + file_name = f"{file_prefix}{i:02d}.bin" + file_id = test_file(file_name=file_name) + file_ids.append(file_id) + + # Query with skip=1, take=3 + query_request = FileLinqQueryRequest( + filter=f'name.StartsWith("{file_prefix}")', + skip=1, + take=3, + order_by=FileLinqQueryOrderBy.CREATED, + order_by_descending=False, + ) + response = client.query_files_linq(query=query_request) + + assert response.available_files is not None + assert response.total_count is not None + assert response.total_count.value == 3 # skip=1, take=3 + assert len(response.available_files) == 3 + + # Verify that we skipped the first file + returned_ids = [f.id for f in response.available_files] + for file_id in returned_ids: + assert file_id in file_ids + def test__search_files__succeeds( self, client: FileClient, test_file, random_filename_extension: str ): From 9db0ab4b459a6f255e2a997b2961ef82a43ef1a9 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Thu, 18 Dec 2025 16:54:52 +0530 Subject: [PATCH 10/17] fix:PRComments --- docs/api_reference/file.rst | 1 + docs/getting_started.rst | 10 +++- examples/file/search_files.py | 51 ++++++++++++++++++- nisystemlink/clients/file/models/__init__.py | 3 +- .../clients/file/models/_base_file_request.py | 25 +++++++++ .../file/models/_base_file_response.py | 4 +- .../clients/file/models/_file_linq_query.py | 24 +-------- .../clients/file/models/_file_metadata.py | 2 +- .../file/models/_search_files_request.py | 24 +-------- tests/integration/file/test_file_client.py | 4 +- 10 files changed, 95 insertions(+), 53 deletions(-) create mode 100644 nisystemlink/clients/file/models/_base_file_request.py diff --git a/docs/api_reference/file.rst b/docs/api_reference/file.rst index 058c5002..0d27a03f 100644 --- a/docs/api_reference/file.rst +++ b/docs/api_reference/file.rst @@ -10,6 +10,7 @@ nisystemlink.clients.file .. automethod:: api_info .. automethod:: get_files .. automethod:: query_files_linq + .. automethod:: search_files .. automethod:: delete_file .. automethod:: delete_files .. automethod:: upload_file diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 4608f959..392fc6eb 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -203,7 +203,7 @@ default connection. The default connection depends on your environment. With a :class:`.FileClient` object, you can: -* Get the list of files, download and delete files +* Get the list of files, query and search for files, download and delete files Examples ~~~~~~~~ @@ -217,6 +217,14 @@ Get the metadata of a File using its Id and download it. Upload a File from disk or memory to SystemLink .. literalinclude:: ../examples/file/upload_file.py + :language: python + :linenos: + +Search for files with filtering and pagination + +.. literalinclude:: ../examples/file/search_files.py + :language: python + :linenos: Feeds API ------- diff --git a/examples/file/search_files.py b/examples/file/search_files.py index 19a78aff..1bb28636 100644 --- a/examples/file/search_files.py +++ b/examples/file/search_files.py @@ -5,6 +5,7 @@ from nisystemlink.clients.core import HttpConfiguration from nisystemlink.clients.file import FileClient, models +from nisystemlink.clients.file.models import UpdateMetadataRequest # Configure connection to SystemLink server server_configuration = HttpConfiguration( @@ -86,7 +87,53 @@ if file.properties: print(f"- {file.properties.get('Name')} (Size: {file.size} bytes)") -# Clean up: delete the test file +# Example 4: Search by multiple custom properties +print("\nExample 4: Search by multiple custom properties") +# First upload a file with custom properties for demonstration +print("Uploading file with custom properties...") +test_file_2 = io.BytesIO(b"Custom properties test file") +test_file_2.name = "custom-props-test.txt" +file_id_2 = client.upload_file(file=test_file_2) + +# Update the file with custom properties + +custom_metadata = UpdateMetadataRequest( + replace_existing=False, + properties={ + "Department": "Engineering", + "Project": "TestAutomation", + "Status": "Active", + "Version": "1.0", + }, +) +client.update_metadata(metadata=custom_metadata, id=file_id_2) + +# Wait for indexing +print("Waiting 5 seconds for custom properties to be indexed...") +time.sleep(5) + +# Search by multiple custom properties using AND operator +search_request = models.SearchFilesRequest( + filter='(properties.Department:"Engineering") AND (properties.Project:"TestAutomation")', + skip=0, + take=10, +) + +response = client.search_files(search_request) +print( + f"Found {response.total_count.value if response.total_count else 0} file(s) with " + "Department=Engineering AND Project=TestAutomation" +) +if response.available_files: + for file in response.available_files: + if file.properties: + print(f"- {file.properties.get('Name')}") + print(f" Department: {file.properties.get('Department')}") + print(f" Project: {file.properties.get('Project')}") + print(f" Status: {file.properties.get('Status')}") + +# Clean up: delete both test files print("\nCleaning up...") client.delete_file(id=file_id) -print(f"Deleted test file with ID: {file_id}") +client.delete_file(id=file_id_2) +print(f"Deleted test files with IDs: {file_id}, {file_id_2}") diff --git a/nisystemlink/clients/file/models/__init__.py b/nisystemlink/clients/file/models/__init__.py index baf28272..8f156e23 100644 --- a/nisystemlink/clients/file/models/__init__.py +++ b/nisystemlink/clients/file/models/__init__.py @@ -1,4 +1,4 @@ -from ._file_metadata import FileMetadata, FileQueryMetadata +from ._file_metadata import FileMetadata, LinqQueryFileMetadata from ._file_query_order_by import FileQueryOrderBy, FileLinqQueryOrderBy from ._file_query_response import FileQueryResponse from ._link import Link @@ -8,5 +8,6 @@ from ._search_files_request import SearchFilesRequest from ._search_files_response import SearchFilesResponse from ._base_file_response import BaseFileResponse, TotalCount +from ._base_file_request import BaseFileRequest # flake8: noqa diff --git a/nisystemlink/clients/file/models/_base_file_request.py b/nisystemlink/clients/file/models/_base_file_request.py new file mode 100644 index 00000000..955bfcc8 --- /dev/null +++ b/nisystemlink/clients/file/models/_base_file_request.py @@ -0,0 +1,25 @@ +from nisystemlink.clients.core._uplink._json_model import JsonModel + + +class BaseFileRequest(JsonModel): + """Base class for file request models containing common query parameters.""" + + filter: str | None = None + """ + Filter string for searching/querying files. + """ + + skip: int | None = None + """ + How many files to skip in the result when paging. + """ + + take: int | None = None + """ + How many files to return in the result. + """ + + order_by_descending: bool | None = False + """ + Whether to sort in descending order. + """ diff --git a/nisystemlink/clients/file/models/_base_file_response.py b/nisystemlink/clients/file/models/_base_file_response.py index c3e0eda4..a4caa3e3 100644 --- a/nisystemlink/clients/file/models/_base_file_response.py +++ b/nisystemlink/clients/file/models/_base_file_response.py @@ -2,7 +2,7 @@ from nisystemlink.clients.core._uplink._json_model import JsonModel -from ._file_metadata import FileQueryMetadata +from ._file_metadata import LinqQueryFileMetadata class TotalCount(JsonModel): @@ -28,7 +28,7 @@ class TotalCount(JsonModel): class BaseFileResponse(JsonModel): """Base class for file response models containing a list of files and total count.""" - available_files: List[FileQueryMetadata] + available_files: List[LinqQueryFileMetadata] """The list of files returned by the query""" total_count: TotalCount diff --git a/nisystemlink/clients/file/models/_file_linq_query.py b/nisystemlink/clients/file/models/_file_linq_query.py index 3c73c4db..0efd9b11 100644 --- a/nisystemlink/clients/file/models/_file_linq_query.py +++ b/nisystemlink/clients/file/models/_file_linq_query.py @@ -1,32 +1,12 @@ -from nisystemlink.clients.core._uplink._json_model import JsonModel +from nisystemlink.clients.file.models._base_file_request import BaseFileRequest from nisystemlink.clients.file.models._base_file_response import BaseFileResponse from nisystemlink.clients.file.models._file_query_order_by import FileLinqQueryOrderBy -class FileLinqQueryRequest(JsonModel): - filter: str | None = None - """ - The filter criteria for files. Consists of a string of queries composed using AND/OR operators. - String values and date strings need to be enclosed in double quotes. Parentheses can be used - around filters to better define the order of operations. - - Example Filter syntax: '[property name][operator][operand] and [property name][operator][operand]' - """ - - skip: int | None = None - """ - How many files to skip in the result when paging. - """ - +class FileLinqQueryRequest(BaseFileRequest): order_by: FileLinqQueryOrderBy | None = None """The property by which to order the files in the response.""" - order_by_descending: bool | None = False - """If true, the files are ordered in descending order based on the property specified in `order_by`.""" - - take: int | None = None - """The maximum number of files to return in the response. Default value is 1000""" - class FileLinqQueryResponse(BaseFileResponse): """Response model for LINQ query operations.""" diff --git a/nisystemlink/clients/file/models/_file_metadata.py b/nisystemlink/clients/file/models/_file_metadata.py index 9a432567..4dfb5c36 100644 --- a/nisystemlink/clients/file/models/_file_metadata.py +++ b/nisystemlink/clients/file/models/_file_metadata.py @@ -85,7 +85,7 @@ class FileMetadata(BaseFileMetadata): """ -class FileQueryMetadata(BaseFileMetadata): +class LinqQueryFileMetadata(BaseFileMetadata): """Metadata for a file returned by a query or search operation.""" updated: datetime | None = None diff --git a/nisystemlink/clients/file/models/_search_files_request.py b/nisystemlink/clients/file/models/_search_files_request.py index 89e01076..751b1670 100644 --- a/nisystemlink/clients/file/models/_search_files_request.py +++ b/nisystemlink/clients/file/models/_search_files_request.py @@ -1,30 +1,10 @@ -from nisystemlink.clients.core._uplink._json_model import JsonModel +from nisystemlink.clients.file.models._base_file_request import BaseFileRequest -class SearchFilesRequest(JsonModel): +class SearchFilesRequest(BaseFileRequest): """Request model for searching files.""" - filter: str | None = None - """ - Filter string for searching files. - """ - - skip: int | None = None - """ - How many files to skip in the result when paging. - """ - - take: int | None = None - """ - How many files to return in the result. - """ - order_by: str | None = None """ The name of the metadata field to sort by. """ - - order_by_descending: bool | None = False - """ - Whether to sort in descending order. - """ diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index 076621e3..9c58121a 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -351,8 +351,6 @@ def search_and_verify() -> None: assert isinstance(file_metadata.updated, datetime) assert file_metadata.workspace is not None assert file_metadata.size is not None - assert file_metadata.properties is not None - assert "Name" in file_metadata.properties # Verify descending order by name returned_names = [ @@ -362,6 +360,8 @@ def search_and_verify() -> None: ] assert returned_names == sorted(returned_names, reverse=True) + search_and_verify() + def test__search_files__no_filter_succeeds(self, client: FileClient, test_file): test_file() From 266c8226f65599cb24700c199edb35a928b9c5d9 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Thu, 18 Dec 2025 17:02:26 +0530 Subject: [PATCH 11/17] fix:BackofRetryCount --- tests/integration/file/test_file_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index 9c58121a..4beaf7a6 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -327,7 +327,7 @@ def test__search_files__succeeds( @backoff.on_exception( backoff.expo, (AssertionError, ApiException), - max_tries=3, + max_tries=5, max_time=10, ) def search_and_verify() -> None: From d1edee962377f727977cb5fbf198bf0ae76e3737 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Thu, 18 Dec 2025 17:14:33 +0530 Subject: [PATCH 12/17] fix:ExampleFile --- examples/file/search_files.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/examples/file/search_files.py b/examples/file/search_files.py index 1bb28636..6feeb559 100644 --- a/examples/file/search_files.py +++ b/examples/file/search_files.py @@ -9,8 +9,8 @@ # Configure connection to SystemLink server server_configuration = HttpConfiguration( - server_uri="https://yourserver.yourcompany.com", - api_key="YourAPIKeyGeneratedFromSystemLink", + server_uri="https://test-api.lifecyclesolutions.ni.com/", + api_key="9voCglXGPBxU6-ZIQyFEwmIAymkdn_ynqOO7t52q2v", ) client = FileClient(configuration=server_configuration) @@ -100,10 +100,8 @@ custom_metadata = UpdateMetadataRequest( replace_existing=False, properties={ - "Department": "Engineering", - "Project": "TestAutomation", - "Status": "Active", - "Version": "1.0", + "TestProperty1": "TestValue1", + "TestProperty2": "TestValue2", }, ) client.update_metadata(metadata=custom_metadata, id=file_id_2) @@ -114,7 +112,7 @@ # Search by multiple custom properties using AND operator search_request = models.SearchFilesRequest( - filter='(properties.Department:"Engineering") AND (properties.Project:"TestAutomation")', + filter='(properties.TestProperty1:"TestValue1") AND (properties.TestProperty2:"TestValue2")', skip=0, take=10, ) @@ -122,15 +120,14 @@ response = client.search_files(search_request) print( f"Found {response.total_count.value if response.total_count else 0} file(s) with " - "Department=Engineering AND Project=TestAutomation" + "TestProperty1=TestValue1 AND TestProperty2=TestValue2" ) if response.available_files: for file in response.available_files: if file.properties: print(f"- {file.properties.get('Name')}") - print(f" Department: {file.properties.get('Department')}") - print(f" Project: {file.properties.get('Project')}") - print(f" Status: {file.properties.get('Status')}") + print(f" TestProperty1: {file.properties.get('TestProperty1')}") + print(f" TestProperty2: {file.properties.get('TestProperty2')}") # Clean up: delete both test files print("\nCleaning up...") From 8800c3c7400314dd518bf41d5c74b39779da0225 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Thu, 18 Dec 2025 18:08:32 +0530 Subject: [PATCH 13/17] fix:PRComments --- examples/file/search_files.py | 22 ++++++------------- nisystemlink/clients/file/models/__init__.py | 2 +- .../clients/file/models/_file_metadata.py | 2 +- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/examples/file/search_files.py b/examples/file/search_files.py index 6feeb559..02e900ea 100644 --- a/examples/file/search_files.py +++ b/examples/file/search_files.py @@ -9,8 +9,8 @@ # Configure connection to SystemLink server server_configuration = HttpConfiguration( - server_uri="https://test-api.lifecyclesolutions.ni.com/", - api_key="9voCglXGPBxU6-ZIQyFEwmIAymkdn_ynqOO7t52q2v", + server_uri="https://yourserver.yourcompany.com", + api_key="YourAPIKeyGeneratedFromSystemLink", ) client = FileClient(configuration=server_configuration) @@ -26,7 +26,6 @@ # Wait for the file to be indexed for search # Note: Files may take a few seconds to appear in search results after upload -print("Waiting 5 seconds for file to be indexed for search...") time.sleep(5) print() @@ -89,14 +88,9 @@ # Example 4: Search by multiple custom properties print("\nExample 4: Search by multiple custom properties") -# First upload a file with custom properties for demonstration -print("Uploading file with custom properties...") -test_file_2 = io.BytesIO(b"Custom properties test file") -test_file_2.name = "custom-props-test.txt" -file_id_2 = client.upload_file(file=test_file_2) - -# Update the file with custom properties +print("Adding custom properties to existing file...") +# Update the existing file with custom properties custom_metadata = UpdateMetadataRequest( replace_existing=False, properties={ @@ -104,10 +98,9 @@ "TestProperty2": "TestValue2", }, ) -client.update_metadata(metadata=custom_metadata, id=file_id_2) +client.update_metadata(metadata=custom_metadata, id=file_id) # Wait for indexing -print("Waiting 5 seconds for custom properties to be indexed...") time.sleep(5) # Search by multiple custom properties using AND operator @@ -129,8 +122,7 @@ print(f" TestProperty1: {file.properties.get('TestProperty1')}") print(f" TestProperty2: {file.properties.get('TestProperty2')}") -# Clean up: delete both test files +# Clean up: delete the test file print("\nCleaning up...") client.delete_file(id=file_id) -client.delete_file(id=file_id_2) -print(f"Deleted test files with IDs: {file_id}, {file_id_2}") +print(f"Deleted test file with ID: {file_id}") diff --git a/nisystemlink/clients/file/models/__init__.py b/nisystemlink/clients/file/models/__init__.py index 8f156e23..a16bd081 100644 --- a/nisystemlink/clients/file/models/__init__.py +++ b/nisystemlink/clients/file/models/__init__.py @@ -1,4 +1,4 @@ -from ._file_metadata import FileMetadata, LinqQueryFileMetadata +from ._file_metadata import FileMetadata from ._file_query_order_by import FileQueryOrderBy, FileLinqQueryOrderBy from ._file_query_response import FileQueryResponse from ._link import Link diff --git a/nisystemlink/clients/file/models/_file_metadata.py b/nisystemlink/clients/file/models/_file_metadata.py index 4dfb5c36..680572a6 100644 --- a/nisystemlink/clients/file/models/_file_metadata.py +++ b/nisystemlink/clients/file/models/_file_metadata.py @@ -86,7 +86,7 @@ class FileMetadata(BaseFileMetadata): class LinqQueryFileMetadata(BaseFileMetadata): - """Metadata for a file returned by a query or search operation.""" + """Metadata for a file returned by a LINQ query.""" updated: datetime | None = None """ From bfb6191501039471fe6f996c19a0355f3f1fd4f2 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Thu, 18 Dec 2025 19:19:43 +0530 Subject: [PATCH 14/17] fix:PRComments --- examples/file/search_files.py | 4 ++-- nisystemlink/clients/file/models/__init__.py | 6 +++++- .../clients/file/models/_base_file_request.py | 7 +++++++ .../clients/file/models/_file_linq_query.py | 6 +++--- .../file/models/_file_query_order_by.py | 20 ++++++++++++++++++- .../file/models/_search_files_request.py | 5 +---- tests/integration/file/test_file_client.py | 3 ++- 7 files changed, 39 insertions(+), 12 deletions(-) diff --git a/examples/file/search_files.py b/examples/file/search_files.py index 02e900ea..6ac9e3a8 100644 --- a/examples/file/search_files.py +++ b/examples/file/search_files.py @@ -5,7 +5,7 @@ from nisystemlink.clients.core import HttpConfiguration from nisystemlink.clients.file import FileClient, models -from nisystemlink.clients.file.models import UpdateMetadataRequest +from nisystemlink.clients.file.models import SearchFilesOrderBy, UpdateMetadataRequest # Configure connection to SystemLink server server_configuration = HttpConfiguration( @@ -54,7 +54,7 @@ filter='name:("search-example*")', skip=0, take=20, - order_by="created", + order_by=SearchFilesOrderBy.CREATED, order_by_descending=True, ) diff --git a/nisystemlink/clients/file/models/__init__.py b/nisystemlink/clients/file/models/__init__.py index a16bd081..e04dcd5f 100644 --- a/nisystemlink/clients/file/models/__init__.py +++ b/nisystemlink/clients/file/models/__init__.py @@ -1,5 +1,9 @@ from ._file_metadata import FileMetadata -from ._file_query_order_by import FileQueryOrderBy, FileLinqQueryOrderBy +from ._file_query_order_by import ( + FileQueryOrderBy, + FileLinqQueryOrderBy, + SearchFilesOrderBy, +) from ._file_query_response import FileQueryResponse from ._link import Link from ._operations import V1Operations diff --git a/nisystemlink/clients/file/models/_base_file_request.py b/nisystemlink/clients/file/models/_base_file_request.py index 955bfcc8..c90af403 100644 --- a/nisystemlink/clients/file/models/_base_file_request.py +++ b/nisystemlink/clients/file/models/_base_file_request.py @@ -1,5 +1,7 @@ from nisystemlink.clients.core._uplink._json_model import JsonModel +from ._file_query_order_by import BaseFileOrderBy + class BaseFileRequest(JsonModel): """Base class for file request models containing common query parameters.""" @@ -19,6 +21,11 @@ class BaseFileRequest(JsonModel): How many files to return in the result. """ + order_by: BaseFileOrderBy | None = None + """ + The property by which to order the files in the response. + """ + order_by_descending: bool | None = False """ Whether to sort in descending order. diff --git a/nisystemlink/clients/file/models/_file_linq_query.py b/nisystemlink/clients/file/models/_file_linq_query.py index 0efd9b11..e9063500 100644 --- a/nisystemlink/clients/file/models/_file_linq_query.py +++ b/nisystemlink/clients/file/models/_file_linq_query.py @@ -1,11 +1,11 @@ from nisystemlink.clients.file.models._base_file_request import BaseFileRequest from nisystemlink.clients.file.models._base_file_response import BaseFileResponse -from nisystemlink.clients.file.models._file_query_order_by import FileLinqQueryOrderBy class FileLinqQueryRequest(BaseFileRequest): - order_by: FileLinqQueryOrderBy | None = None - """The property by which to order the files in the response.""" + """Request model for LINQ query operations.""" + + pass class FileLinqQueryResponse(BaseFileResponse): diff --git a/nisystemlink/clients/file/models/_file_query_order_by.py b/nisystemlink/clients/file/models/_file_query_order_by.py index 4ce9cb3c..f61c4e66 100644 --- a/nisystemlink/clients/file/models/_file_query_order_by.py +++ b/nisystemlink/clients/file/models/_file_query_order_by.py @@ -10,9 +10,27 @@ class FileQueryOrderBy(Enum): LAST_UPDATED_TIMESTAMP = "lastUpdatedTimestamp" -class FileLinqQueryOrderBy(Enum): +class BaseFileOrderBy(Enum): + """Base enum for file ordering options.""" + + pass + + +class FileLinqQueryOrderBy(BaseFileOrderBy): """Order Files LINQ Query by Metadata for POST /query-files-linq endpoint.""" + NAME = "name" + CREATED = "created" + UPDATED = "updated" + EXTENSION = "extension" + SIZE = "size" + WORKSPACE = "workspace" + + +class SearchFilesOrderBy(BaseFileOrderBy): + """Order Files Search by Metadata for POST /search-files endpoint.""" + + NAME = "name" CREATED = "created" UPDATED = "updated" EXTENSION = "extension" diff --git a/nisystemlink/clients/file/models/_search_files_request.py b/nisystemlink/clients/file/models/_search_files_request.py index 751b1670..9d055b83 100644 --- a/nisystemlink/clients/file/models/_search_files_request.py +++ b/nisystemlink/clients/file/models/_search_files_request.py @@ -4,7 +4,4 @@ class SearchFilesRequest(BaseFileRequest): """Request model for searching files.""" - order_by: str | None = None - """ - The name of the metadata field to sort by. - """ + pass diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index 4beaf7a6..9758f350 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -13,6 +13,7 @@ from nisystemlink.clients.file.models import ( FileLinqQueryOrderBy, FileLinqQueryRequest, + SearchFilesOrderBy, SearchFilesRequest, UpdateMetadataRequest, ) @@ -319,7 +320,7 @@ def test__search_files__succeeds( filter=f'(name: ("{file_prefix}*"))', skip=1, take=3, - order_by="name", + order_by=SearchFilesOrderBy.NAME, order_by_descending=True, ) From 2607444fc5039da8f784f02549afefb1bbefadb5 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Mon, 22 Dec 2025 17:01:16 +0530 Subject: [PATCH 15/17] fix:PRComments --- nisystemlink/clients/file/_file_client.py | 8 +++++++- nisystemlink/clients/file/models/__init__.py | 2 +- .../clients/file/models/_base_file_request.py | 4 ++-- .../file/models/_base_file_response.py | 20 +++++++++++-------- .../file/models/_file_query_order_by.py | 10 ++-------- tests/integration/file/test_file_client.py | 18 +++++++++++++++-- 6 files changed, 40 insertions(+), 22 deletions(-) diff --git a/nisystemlink/clients/file/_file_client.py b/nisystemlink/clients/file/_file_client.py index 35c6f64d..e88a3401 100644 --- a/nisystemlink/clients/file/_file_client.py +++ b/nisystemlink/clients/file/_file_client.py @@ -169,6 +169,11 @@ def search_files( ) -> models.SearchFilesResponse: """Search for files based on filter criteria. + Note: + This endpoint requires Elasticsearch to be available in the SystemLink cluster. + If Elasticsearch is not configured, this method will fail with an ApiException. + For deployments without Elasticsearch, use `query_files_linq()` instead. + Args: request: The search request containing filter, pagination, and sorting parameters. @@ -176,7 +181,8 @@ def search_files( SearchFilesResponse: Response containing matching files and total count. Raises: - ApiException: if unable to communicate with the File Service. + ApiException: if unable to communicate with the File Service or if Elasticsearch + is not available in the cluster. """ ... diff --git a/nisystemlink/clients/file/models/__init__.py b/nisystemlink/clients/file/models/__init__.py index e04dcd5f..9624128b 100644 --- a/nisystemlink/clients/file/models/__init__.py +++ b/nisystemlink/clients/file/models/__init__.py @@ -11,7 +11,7 @@ from ._file_linq_query import FileLinqQueryRequest, FileLinqQueryResponse from ._search_files_request import SearchFilesRequest from ._search_files_response import SearchFilesResponse -from ._base_file_response import BaseFileResponse, TotalCount +from ._base_file_response import BaseFileResponse, TotalCount, TotalCountRelation from ._base_file_request import BaseFileRequest # flake8: noqa diff --git a/nisystemlink/clients/file/models/_base_file_request.py b/nisystemlink/clients/file/models/_base_file_request.py index c90af403..f5f76b3d 100644 --- a/nisystemlink/clients/file/models/_base_file_request.py +++ b/nisystemlink/clients/file/models/_base_file_request.py @@ -1,6 +1,6 @@ from nisystemlink.clients.core._uplink._json_model import JsonModel -from ._file_query_order_by import BaseFileOrderBy +from ._file_query_order_by import FileLinqQueryOrderBy, SearchFilesOrderBy class BaseFileRequest(JsonModel): @@ -21,7 +21,7 @@ class BaseFileRequest(JsonModel): How many files to return in the result. """ - order_by: BaseFileOrderBy | None = None + order_by: FileLinqQueryOrderBy | SearchFilesOrderBy | None = None """ The property by which to order the files in the response. """ diff --git a/nisystemlink/clients/file/models/_base_file_response.py b/nisystemlink/clients/file/models/_base_file_response.py index a4caa3e3..9bf23c26 100644 --- a/nisystemlink/clients/file/models/_base_file_response.py +++ b/nisystemlink/clients/file/models/_base_file_response.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import List from nisystemlink.clients.core._uplink._json_model import JsonModel @@ -5,20 +6,23 @@ from ._file_metadata import LinqQueryFileMetadata +class TotalCountRelation(str, Enum): + """Describes the relation the returned total count value has with respect to the total number of files.""" + + EQUALS = "eq" + """Equals, meaning that the returned items are all the items that matched the filter.""" + + GREATER_THAN_OR_EQUAL = "gte" + """Greater or equal, meaning that the take limit has been hit, but there are further items that match the query.""" + + class TotalCount(JsonModel): """The total number of files that match the query regardless of skip and take values""" - relation: str + relation: TotalCountRelation """ Describes the relation the returned total count value has with respect to the total number of files matched by the query. - - Possible values: - - - "eq" -> equals, meaning that the returned items are all the items that matched the filter. - - - "gte" -> greater or equal, meaning that there the take limit has been hit, but there are further - items that match the query in the database. """ value: int diff --git a/nisystemlink/clients/file/models/_file_query_order_by.py b/nisystemlink/clients/file/models/_file_query_order_by.py index f61c4e66..6cd889dd 100644 --- a/nisystemlink/clients/file/models/_file_query_order_by.py +++ b/nisystemlink/clients/file/models/_file_query_order_by.py @@ -10,13 +10,7 @@ class FileQueryOrderBy(Enum): LAST_UPDATED_TIMESTAMP = "lastUpdatedTimestamp" -class BaseFileOrderBy(Enum): - """Base enum for file ordering options.""" - - pass - - -class FileLinqQueryOrderBy(BaseFileOrderBy): +class FileLinqQueryOrderBy(str, Enum): """Order Files LINQ Query by Metadata for POST /query-files-linq endpoint.""" NAME = "name" @@ -27,7 +21,7 @@ class FileLinqQueryOrderBy(BaseFileOrderBy): WORKSPACE = "workspace" -class SearchFilesOrderBy(BaseFileOrderBy): +class SearchFilesOrderBy(str, Enum): """Order Files Search by Metadata for POST /search-files endpoint.""" NAME = "name" diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index 9758f350..a21f7937 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -15,6 +15,7 @@ FileLinqQueryRequest, SearchFilesOrderBy, SearchFilesRequest, + TotalCountRelation, UpdateMetadataRequest, ) from nisystemlink.clients.file.utilities import rename_file @@ -230,7 +231,7 @@ def test__query_files_linq__filter_by_name_succeeds( assert response.available_files is not None assert response.total_count is not None assert response.total_count.value == 1 - assert response.total_count.relation == "eq" + assert response.total_count.relation == TotalCountRelation.EQUALS assert len(response.available_files) == 1 assert response.available_files[0].id == file_id assert response.available_files[0].created is not None @@ -265,7 +266,7 @@ def test__query_files_linq__filter_returns_no_results(self, client: FileClient): assert len(response.available_files) == 0 assert response.total_count is not None assert response.total_count.value == 0 - assert response.total_count.relation == "eq" + assert response.total_count.relation == TotalCountRelation.EQUALS def test__query_files_linq__skip_and_take_pagination( self, client: FileClient, test_file @@ -301,6 +302,15 @@ def test__query_files_linq__skip_and_take_pagination( for file_id in returned_ids: assert file_id in file_ids + # Verify skip=1 excluded the first file in creation order + returned_names = [ + f.properties.get("Name", "") + for f in response.available_files + if f.properties + ] + expected_skipped_file = f"{file_prefix}00.bin" + assert expected_skipped_file not in returned_names + def test__search_files__succeeds( self, client: FileClient, test_file, random_filename_extension: str ): @@ -361,6 +371,10 @@ def search_and_verify() -> None: ] assert returned_names == sorted(returned_names, reverse=True) + # Verify skip=1 excluded the first file in descending order + expected_skipped_file = f"{file_prefix}_4.bin" + assert expected_skipped_file not in returned_names + search_and_verify() def test__search_files__no_filter_succeeds(self, client: FileClient, test_file): From 31d6f4e666f301d3c4e6501571ed2b2181dc8fe4 Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Mon, 22 Dec 2025 20:21:53 +0530 Subject: [PATCH 16/17] fix:PRComments --- tests/integration/file/test_file_client.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index a21f7937..6f1658d6 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -314,7 +314,12 @@ def test__query_files_linq__skip_and_take_pagination( def test__search_files__succeeds( self, client: FileClient, test_file, random_filename_extension: str ): - """Test search_files with filtering, pagination, and ordering.""" + """Test search_files with filtering, pagination, and ordering. + + Note: search_files() is not guaranteed to return newly created files immediately + due to indexing delay (a few seconds). Retry logic with backoff is used to handle + this eventual consistency behavior. + """ # Upload 5 files to test various scenarios NUM_FILES = 5 file_ids = [] From d701167c2336220fb4caf17e29d7c20e8d0730cb Mon Sep 17 00:00:00 2001 From: RSam-NI Date: Tue, 23 Dec 2025 13:20:19 +0530 Subject: [PATCH 17/17] fix:PRComments --- .../clients/file/models/_base_file_request.py | 7 ------ .../clients/file/models/_file_linq_query.py | 6 ++++- .../file/models/_search_files_request.py | 6 ++++- tests/integration/file/test_file_client.py | 22 +++++++++++++++++++ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/nisystemlink/clients/file/models/_base_file_request.py b/nisystemlink/clients/file/models/_base_file_request.py index f5f76b3d..955bfcc8 100644 --- a/nisystemlink/clients/file/models/_base_file_request.py +++ b/nisystemlink/clients/file/models/_base_file_request.py @@ -1,7 +1,5 @@ from nisystemlink.clients.core._uplink._json_model import JsonModel -from ._file_query_order_by import FileLinqQueryOrderBy, SearchFilesOrderBy - class BaseFileRequest(JsonModel): """Base class for file request models containing common query parameters.""" @@ -21,11 +19,6 @@ class BaseFileRequest(JsonModel): How many files to return in the result. """ - order_by: FileLinqQueryOrderBy | SearchFilesOrderBy | None = None - """ - The property by which to order the files in the response. - """ - order_by_descending: bool | None = False """ Whether to sort in descending order. diff --git a/nisystemlink/clients/file/models/_file_linq_query.py b/nisystemlink/clients/file/models/_file_linq_query.py index e9063500..640b31ad 100644 --- a/nisystemlink/clients/file/models/_file_linq_query.py +++ b/nisystemlink/clients/file/models/_file_linq_query.py @@ -1,11 +1,15 @@ from nisystemlink.clients.file.models._base_file_request import BaseFileRequest from nisystemlink.clients.file.models._base_file_response import BaseFileResponse +from nisystemlink.clients.file.models._file_query_order_by import FileLinqQueryOrderBy class FileLinqQueryRequest(BaseFileRequest): """Request model for LINQ query operations.""" - pass + order_by: FileLinqQueryOrderBy | None = None + """ + The property by which to order the files in the response. + """ class FileLinqQueryResponse(BaseFileResponse): diff --git a/nisystemlink/clients/file/models/_search_files_request.py b/nisystemlink/clients/file/models/_search_files_request.py index 9d055b83..c7371600 100644 --- a/nisystemlink/clients/file/models/_search_files_request.py +++ b/nisystemlink/clients/file/models/_search_files_request.py @@ -1,7 +1,11 @@ from nisystemlink.clients.file.models._base_file_request import BaseFileRequest +from nisystemlink.clients.file.models._file_query_order_by import SearchFilesOrderBy class SearchFilesRequest(BaseFileRequest): """Request model for searching files.""" - pass + order_by: SearchFilesOrderBy | None = None + """ + The property by which to order the files in the response. + """ diff --git a/tests/integration/file/test_file_client.py b/tests/integration/file/test_file_client.py index 6f1658d6..84aab477 100644 --- a/tests/integration/file/test_file_client.py +++ b/tests/integration/file/test_file_client.py @@ -268,6 +268,28 @@ def test__query_files_linq__filter_returns_no_results(self, client: FileClient): assert response.total_count.value == 0 assert response.total_count.relation == TotalCountRelation.EQUALS + def test__query_files_linq__total_count_relation_accepts_string( + self, client: FileClient, test_file, random_filename_extension + ): + """Test backward compatibility: TotalCountRelation should accept string values. + + TotalCountRelation was previously a plain str type. This test ensures that string + values like 'eq' and 'gte' are still accepted for backward compatibility. + """ + test_file(file_name=random_filename_extension) + + query_request = FileLinqQueryRequest( + filter=f'name == "{random_filename_extension}"', + ) + response = client.query_files_linq(query=query_request) + + assert response.total_count is not None + # Test that the relation can be compared with string values + assert response.total_count.relation == "eq" + assert response.total_count.relation in ["eq", "gte"] + # Also verify enum comparison still works + assert response.total_count.relation == TotalCountRelation.EQUALS # type: ignore[comparison-overlap] + def test__query_files_linq__skip_and_take_pagination( self, client: FileClient, test_file ):