diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 5049c5c..960c595 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -10,14 +10,14 @@ jobs: services: api: - image: ghcr.io/better-hpc/keystone-api + image: docker.cloudsmith.io/better-hpc/keystone/keystone-api ports: - 8000:8000 strategy: fail-fast: false matrix: - python-version: [ "3.9", "3.10", "3.11", "3.12" , "3.13" ] + python-version: ${{ fromJson(vars.PYTHON_VERSIONS) }} steps: - name: Checkout repository @@ -46,7 +46,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.9", "3.10", "3.11", "3.12" , "3.13" ] + python-version: ${{ fromJson(vars.PYTHON_VERSIONS) }} steps: - name: Checkout repository diff --git a/keystone_client/client.py b/keystone_client/client.py index 115d73a..b44305d 100644 --- a/keystone_client/client.py +++ b/keystone_client/client.py @@ -6,7 +6,7 @@ """ import abc -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable import httpx from httpx import HTTPStatusError @@ -74,10 +74,10 @@ def _delete_factory(self, endpoint: Endpoint) -> Callable: @staticmethod def _preprocess_filters( - filters: Optional[Dict[str, Any]], - search: Optional[str], - order: Optional[str] - ) -> Optional[Dict[str, Any]]: + filters: dict[str, Any] | None, + search: str | None, + order: str | None + ) -> dict[str, Any] | None: """Inject _search and _order into query parameters.""" if search is None and order is None: @@ -110,7 +110,7 @@ def _handle_identity_response(response: httpx.Response) -> dict: return response.json() @staticmethod - def _handle_retrieve_response(response: httpx.Response) -> Optional[dict]: + def _handle_retrieve_response(response: httpx.Response) -> dict | None: """Handle the HTTP response for a record retrieval request. Args: @@ -218,7 +218,7 @@ def is_authenticated(self, timeout: int = httpx.USE_CLIENT_DEFAULT) -> dict: def _create_factory(self, endpoint: Endpoint) -> Callable: """Factory function for data creation methods.""" - def create_record(data: Optional[RequestData] = None, files: Optional[RequestFiles] = None) -> dict: + def create_record(data: RequestData | None = None, files: RequestFiles | None = None) -> dict: """Create a new API record. Args: @@ -239,12 +239,12 @@ def _retrieve_factory(self, endpoint: Endpoint) -> Callable: """Factory function for data retrieval methods.""" def retrieve_record( - pk: Optional[int] = None, - filters: Optional[Dict[str, Any]] = None, - search: Optional[str] = None, - order: Optional[str] = None, + pk: int | None = None, + filters: dict[str, Any] | None = None, + search: str | None = None, + order: str | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT - ) -> Union[None, dict, list[dict]]: + ) -> list[dict] | dict | None: """Retrieve one or more API records. A single record is returned when specifying a primary key, otherwise the returned @@ -274,8 +274,8 @@ def _update_factory(self, endpoint: Endpoint) -> Callable: def update_record( pk: int, - data: Optional[RequestData] = None, - files: Optional[RequestFiles] = None + data: RequestData | None = None, + files: RequestFiles | None = None ) -> dict: """Update partial values for an existing API record. @@ -369,7 +369,7 @@ async def is_authenticated(self, timeout: int = httpx.USE_CLIENT_DEFAULT) -> dic def _create_factory(self, endpoint: Endpoint) -> Callable: """Factory function for data creation methods.""" - async def create_record(data: Optional[RequestData] = None, files: Optional[RequestFiles] = None) -> dict: + async def create_record(data: RequestData | None = None, files: RequestFiles | None = None) -> dict: """Create a new API record. Args: @@ -390,12 +390,12 @@ def _retrieve_factory(self, endpoint: Endpoint) -> Callable: """Factory function for data retrieval methods.""" async def retrieve_record( - pk: Optional[int] = None, - filters: Optional[Dict[str, Any]] = None, - search: Optional[str] = None, - order: Optional[str] = None, + pk: int | None = None, + filters: dict[str, Any] | None = None, + search: str | None = None, + order: str | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT - ) -> Union[None, dict, list[dict]]: + ) -> list[dict] | dict | None: """Retrieve one or more API records. A single record is returned when specifying a primary key, otherwise the returned @@ -425,8 +425,8 @@ def _update_factory(self, endpoint: Endpoint) -> Callable: async def update_record( pk: int, - data: Optional[RequestData] = None, - files: Optional[RequestFiles] = None + data: RequestData | None = None, + files: RequestFiles | None = None ) -> dict: """Update partial values for an existing API record. diff --git a/keystone_client/http.py b/keystone_client/http.py index d1c9deb..ae9d980 100644 --- a/keystone_client/http.py +++ b/keystone_client/http.py @@ -11,7 +11,7 @@ import logging import re import uuid -from typing import Literal, Optional, Union +from typing import Literal from urllib.parse import urljoin, urlparse import httpx @@ -38,9 +38,9 @@ def __init__( verify_ssl: bool = True, follow_redirects: bool = False, max_redirects: int = 10, - timeout: Optional[int] = 15, + timeout: int | None = 15, limits: httpx.Limits = httpx.Limits(max_connections=100, max_keepalive_connections=20), - transport: Optional[httpx.BaseTransport] = None, + transport: httpx.BaseTransport | None = None, ) -> None: """Initialize a new HTTP session. @@ -97,7 +97,7 @@ def normalize_url(url: str) -> str: path = re.sub(r"/{2,}", "/", parts.path).rstrip("/") + "/" return parts._replace(path=path).geturl() - def get_application_headers(self, overrides: Union[dict, None] = None) -> dict[str, str]: + def get_application_headers(self, overrides: dict | None = None) -> dict[str, str]: """Return application-specific headers for the current session.""" headers = {self.CID_HEADER: self._cid} @@ -110,7 +110,7 @@ def get_application_headers(self, overrides: Union[dict, None] = None) -> dict[s return headers @abc.abstractmethod - def _client_factory(self, **kwargs) -> Union[httpx.Client, httpx.AsyncClient]: + def _client_factory(self, **kwargs) -> httpx.Client | httpx.AsyncClient: """Create a new HTTP client instance with the provided settings.""" @abc.abstractmethod @@ -123,10 +123,10 @@ def send_request( method: HttpMethod, endpoint: str, *, - headers: Optional[dict] = None, - json: Optional[RequestContent] = None, - files: Optional[RequestFiles] = None, - params: Optional[QueryParamTypes] = None, + headers: dict | None = None, + json: RequestContent | None = None, + files: RequestFiles | None = None, + params: QueryParamTypes | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send an HTTP request (sync or async depending on the implementation).""" @@ -135,7 +135,7 @@ def send_request( def http_get( self, endpoint: str, - params: Optional[QueryParamTypes] = None, + params: QueryParamTypes | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send a GET request.""" @@ -144,8 +144,8 @@ def http_get( def http_post( self, endpoint: str, - json: Optional[RequestData] = None, - files: Optional[RequestFiles] = None, + json: RequestData | None = None, + files: RequestFiles | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send a POST request.""" @@ -154,8 +154,8 @@ def http_post( def http_patch( self, endpoint: str, - json: Optional[RequestData] = None, - files: Optional[RequestFiles] = None, + json: RequestData | None = None, + files: RequestFiles | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send a PATCH request.""" @@ -164,8 +164,8 @@ def http_patch( def http_put( self, endpoint: str, - json: Optional[RequestData] = None, - files: Optional[RequestFiles] = None, + json: RequestData | None = None, + files: RequestFiles | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send a PUT request.""" @@ -206,9 +206,9 @@ def send_request( endpoint: str, *, headers: dict = None, - json: Optional[RequestContent] = None, - files: Optional[RequestFiles] = None, - params: Optional[QueryParamTypes] = None, + json: RequestContent | None = None, + files: RequestFiles | None = None, + params: QueryParamTypes | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send an HTTP request. @@ -243,7 +243,7 @@ def send_request( def http_get( self, endpoint: str, - params: Optional[QueryParamTypes] = None, + params: QueryParamTypes | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send a GET request to an API endpoint. @@ -262,8 +262,8 @@ def http_get( def http_post( self, endpoint: str, - json: Optional[RequestData] = None, - files: Optional[RequestFiles] = None, + json: RequestData | None = None, + files: RequestFiles | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send a POST request to an API endpoint. @@ -283,8 +283,8 @@ def http_post( def http_patch( self, endpoint: str, - json: Optional[RequestData] = None, - files: Optional[RequestFiles] = None, + json: RequestData | None = None, + files: RequestFiles | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send a PATCH request to an API endpoint. @@ -304,8 +304,8 @@ def http_patch( def http_put( self, endpoint: str, - json: Optional[RequestData] = None, - files: Optional[RequestFiles] = None, + json: RequestData | None = None, + files: RequestFiles | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send a PUT request to an API endpoint. @@ -363,9 +363,9 @@ async def send_request( endpoint: str, *, headers: dict = None, - json: Optional[dict] = None, - files: Optional[RequestFiles] = None, - params: Optional[QueryParamTypes] = None, + json: dict | None = None, + files: RequestFiles | None = None, + params: QueryParamTypes | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send an HTTP request. @@ -400,7 +400,7 @@ async def send_request( async def http_get( self, endpoint: str, - params: Optional[QueryParamTypes] = None, + params: QueryParamTypes | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send an asynchronous GET request to an API endpoint. @@ -419,8 +419,8 @@ async def http_get( async def http_post( self, endpoint: str, - json: Optional[RequestData] = None, - files: Optional[RequestFiles] = None, + json: RequestData | None = None, + files: RequestFiles | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send an asynchronous POST request to an API endpoint. @@ -440,8 +440,8 @@ async def http_post( async def http_patch( self, endpoint: str, - json: Optional[RequestData] = None, - files: Optional[RequestFiles] = None, + json: RequestData | None = None, + files: RequestFiles | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send an asynchronous PATCH request to an API endpoint. @@ -461,8 +461,8 @@ async def http_patch( async def http_put( self, endpoint: str, - json: Optional[RequestData] = None, - files: Optional[RequestFiles] = None, + json: RequestData | None = None, + files: RequestFiles | None = None, timeout: int = httpx.USE_CLIENT_DEFAULT, ) -> httpx.Response: """Send an asynchronous PUT request to an API endpoint. diff --git a/poetry.lock b/poetry.lock index e25001c..a3b61d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "anyio" @@ -148,7 +148,7 @@ files = [ ] [package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +toml = ["tomli"] [[package]] name = "exceptiongroup" @@ -222,7 +222,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -270,5 +270,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = "^3.9" -content-hash = "df81c015e75df93b504193bdb6c5584b75bc4ef0b8d800a14a84add5ab653aa4" +python-versions = "^3.10" +content-hash = "cfd0a229d6b27bc129be0b7fe2747208a2b86cf7ddafba6002453b9ee258c5b1" diff --git a/pyproject.toml b/pyproject.toml index 710600e..55fc227 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.10" httpx = "^0.28.1" [tool.poetry.group.tests] diff --git a/tests/function_tests/utils.py b/tests/function_tests/config.py similarity index 100% rename from tests/function_tests/utils.py rename to tests/function_tests/config.py diff --git a/tests/function_tests/test_async.py b/tests/function_tests/test_async.py index 4474bf1..2efcb24 100644 --- a/tests/function_tests/test_async.py +++ b/tests/function_tests/test_async.py @@ -5,7 +5,7 @@ import httpx from keystone_client import AsyncKeystoneClient -from tests.function_tests.utils import API_HOST, API_PASSWORD, API_USER +from tests.function_tests.config import API_HOST, API_PASSWORD, API_USER class Authentication(IsolatedAsyncioTestCase): @@ -83,244 +83,3 @@ async def test_authenticated_user(self) -> None: await self.client.login(API_USER, API_PASSWORD) user_meta = await self.client.is_authenticated() self.assertEqual(API_USER, user_meta['username']) - - -class Create(IsolatedAsyncioTestCase): - """Test record creation via the `create_cluster` method.""" - - async def asyncSetUp(self) -> None: - """Authenticate a new API client instance.""" - - self.client = AsyncKeystoneClient(API_HOST) - await self.client.login(API_USER, API_PASSWORD) - await self._clear_old_records() - - async def asyncTearDown(self) -> None: - """Close any open server connections.""" - - await self._clear_old_records() - await self.client.close() - - async def _clear_old_records(self) -> None: - """Delete any test records.""" - - cluster_list = await self.client.http_get(f'allocations/clusters/', params={'name': 'Test-Cluster'}) - for cluster in cluster_list.json(): - delete = await self.client.http_delete(f"allocations/clusters/{cluster['id']}/") - delete.raise_for_status() - - async def test_record_is_created(self) -> None: - """Test a record is created successfully.""" - - new_record_data = await self.client.create_cluster({ - 'name': 'Test-Cluster', - 'description': 'Cluster created for testing purposes.' - }) - - pk = new_record_data['id'] - get_cluster = await self.client.http_get(f'allocations/clusters/{pk}/') - get_cluster.raise_for_status() - - async def test_record_data_matches_request(self) -> None: - """Test the returned record data matches the request.""" - - expected_data = {'name': 'Test-Cluster', 'description': 'Cluster created for testing purposes.'} - new_record_data = await self.client.create_cluster(expected_data) - - self.assertEqual(expected_data['name'], new_record_data['name']) - self.assertEqual(expected_data['description'], new_record_data['description']) - - async def test_error_on_failure(self) -> None: - """Test an error is raised when record creation fails.""" - - with self.assertRaises(httpx.HTTPError): - await self.client.create_cluster() - - -class Retrieve(IsolatedAsyncioTestCase): - """Test record retrieval via the `retrieve_cluster` method.""" - - async def asyncSetUp(self) -> None: - """Authenticate a new API client instance.""" - - self.client = AsyncKeystoneClient(API_HOST) - await self.client.login(API_USER, API_PASSWORD) - - await self._clear_old_records() - - self.test_cluster = await self.client.create_cluster({ - 'name': 'Test-Cluster', - 'description': 'Cluster created for retrieval testing purposes.' - }) - - self.other_cluster = await self.client.create_cluster({ - 'name': 'Other-Cluster', - 'description': 'Another cluster created for testing purposes.' - }) - - async def asyncTearDown(self) -> None: - """Delete any test records.""" - - await self._clear_old_records() - await self.client.close() - - async def _clear_old_records(self) -> None: - """Delete any test records.""" - - cluster_list = await self.client.http_get( - f'allocations/clusters/', params={'name__in': 'Test-Cluster,Other-Cluster'}) - - for cluster in cluster_list.json(): - delete = await self.client.http_delete(f"allocations/clusters/{cluster['id']}/") - delete.raise_for_status() - - async def test_retrieve_by_pk(self) -> None: - """Test the retrieval of a specific record via its primary key.""" - - pk = self.test_cluster['id'] - retrieved_cluster = await self.client.retrieve_cluster(pk=pk) - self.assertIsNotNone(retrieved_cluster) - self.assertEqual(retrieved_cluster['id'], pk) - - async def test_retrieve_with_filters(self) -> None: - """Test the filtering of returned records via search params.""" - - retrieved_clusters = await self.client.retrieve_cluster(filters={"name": "Test-Cluster"}) - self.assertIsInstance(retrieved_clusters, list) - self.assertTrue(all(cluster['name'] == "Test-Cluster" for cluster in retrieved_clusters)) - - async def test_retrieve_all(self) -> None: - """Test the retrieval of all records.""" - - all_clusters = await self.client.retrieve_cluster() - self.assertIsInstance(all_clusters, list) - self.assertGreater(len(all_clusters), 0) - - async def test_none_on_missing_record(self) -> None: - """Test `None` is returned when a record does not exist.""" - - missing_cluster = await self.client.retrieve_cluster(pk=999999) - self.assertIsNone(missing_cluster) - - async def test_error_on_failure(self) -> None: - """Test an error is raised when record retrieval fails.""" - - # Use an unauthenticated client session on an endpoint requiring authentication - async with AsyncKeystoneClient(API_HOST) as client: - with self.assertRaises(httpx.HTTPError): - await client.retrieve_cluster() - - -class Update(IsolatedAsyncioTestCase): - """Test record updates via the `update_cluster` method.""" - - async def asyncSetUp(self) -> None: - """Create records for testing.""" - - self.client = AsyncKeystoneClient(API_HOST) - await self.client.login(API_USER, API_PASSWORD) - - await self._clear_old_records() - self.test_cluster = await self.client.create_cluster({ - 'name': 'Test-Cluster', - 'description': 'Cluster created for update testing purposes.' - }) - - async def asyncTearDown(self) -> None: - """Delete any test records.""" - - await self._clear_old_records() - await self.client.close() - - async def _clear_old_records(self) -> None: - """Delete any test records.""" - - cluster_list = await self.client.http_get(f'allocations/clusters/', params={'name': 'Test-Cluster'}) - for cluster in cluster_list.json(): - delete = await self.client.http_delete(f"allocations/clusters/{cluster['id']}/") - delete.raise_for_status() - - async def test_update_record(self) -> None: - """Test the record is updated successfully.""" - - pk = self.test_cluster['id'] - updated_data = {'description': "Updated description"} - updated_record = await self.client.update_cluster(pk=pk, data=updated_data) - self.assertIsNotNone(updated_record) - self.assertEqual(updated_record['description'], "Updated description") - - async def test_update_nonexistent_record(self) -> None: - """Test updating a nonexistent record raises an error.""" - - with self.assertRaises(httpx.HTTPError): - await self.client.update_cluster(pk=999999, data={'description': "This should fail"}) - - async def test_partial_update(self) -> None: - """Test a partial update modifies only specified fields.""" - - pk = self.test_cluster['id'] - original_status = self.test_cluster['enabled'] - - updated_data = {'description': "Partially updated description"} - updated_record = await self.client.update_cluster(pk=pk, data=updated_data) - - self.assertEqual(updated_record['description'], "Partially updated description") - self.assertEqual(updated_record['enabled'], original_status) - - async def test_no_update_on_empty_data(self) -> None: - """Test an empty update request does not change the record.""" - - pk = self.test_cluster['id'] - original_data = self.test_cluster.copy() - updated_record = await self.client.update_cluster(pk=pk, data={}) - self.assertEqual(updated_record, original_data) - - -class Delete(IsolatedAsyncioTestCase): - """Test record deletion using the `delete_cluster` method.""" - - async def asyncSetUp(self) -> None: - """Create records for testing.""" - - self.client = AsyncKeystoneClient(API_HOST) - await self.client.login(API_USER, API_PASSWORD) - - await self._clear_old_records() - self.test_cluster = await self.client.create_cluster({ - 'name': 'Test-Cluster', - 'description': 'Cluster created for delete testing purposes.' - }) - - async def asyncTearDown(self) -> None: - """Delete any test records.""" - - await self._clear_old_records() - await self.client.close() - - async def _clear_old_records(self) -> None: - """Delete any test records.""" - - cluster_list = await self.client.http_get(f'allocations/clusters/', params={'name': 'Test-Cluster'}) - for cluster in cluster_list.json(): - response = await self.client.http_delete(f"allocations/clusters/{cluster['id']}/") - response.raise_for_status() - - async def test_delete_record(self) -> None: - """Test a record is deleted successfully.""" - - pk = self.test_cluster['id'] - await self.client.delete_cluster(pk=pk) - - retrieved_cluster = await self.client.retrieve_cluster(pk=pk) - self.assertIsNone(retrieved_cluster) - - async def test_delete_nonexistent_record_silent(self) -> None: - """Test deleting a nonexistent record exits silently.""" - - await self.client.delete_cluster(pk=999999) - - async def test_delete_nonexistent_record_raise(self) -> None: - """Test deleting a nonexistent record raises an exception when specified.""" - - with self.assertRaises(httpx.HTTPError): - await self.client.delete_cluster(pk=999999, raise_not_exists=True) diff --git a/tests/function_tests/test_sync.py b/tests/function_tests/test_sync.py index 81f5d0e..9502566 100644 --- a/tests/function_tests/test_sync.py +++ b/tests/function_tests/test_sync.py @@ -5,7 +5,7 @@ import httpx from keystone_client import KeystoneClient -from tests.function_tests.utils import API_HOST, API_PASSWORD, API_USER +from tests.function_tests.config import API_HOST, API_PASSWORD, API_USER class Authentication(TestCase): @@ -83,243 +83,3 @@ def test_authenticated_user(self) -> None: self.client.login(API_USER, API_PASSWORD) user_meta = self.client.is_authenticated() self.assertEqual(API_USER, user_meta['username']) - - -class Create(TestCase): - """Test record creation via the `create_cluster` method.""" - - def setUp(self) -> None: - """Authenticate a new API client instance.""" - - self.client = KeystoneClient(API_HOST) - self.client.login(API_USER, API_PASSWORD) - self._clear_old_records() - - def tearDown(self) -> None: - """Delete any test records.""" - - self._clear_old_records() - self.client.close() - - def _clear_old_records(self) -> None: - """Delete any test records.""" - - cluster_list = self.client.http_get(f'allocations/clusters/', params={'name': 'Test-Cluster'}) - for cluster in cluster_list.json(): - delete = self.client.http_delete(f"allocations/clusters/{cluster['id']}/") - delete.raise_for_status() - - def test_record_is_created(self) -> None: - """Test a record is created successfully.""" - - new_record_data = self.client.create_cluster({ - 'name': 'Test-Cluster', - 'description': 'Cluster created for testing purposes.' - }) - - pk = new_record_data['id'] - get_cluster = self.client.http_get(f'allocations/clusters/{pk}/') - get_cluster.raise_for_status() - - def test_record_data_matches_request(self) -> None: - """Test the returned record data matches the request.""" - - expected_data = {'name': 'Test-Cluster', 'description': 'Cluster created for testing purposes.'} - new_record_data = self.client.create_cluster(expected_data) - - self.assertEqual(expected_data['name'], new_record_data['name']) - self.assertEqual(expected_data['description'], new_record_data['description']) - - def test_error_on_failure(self) -> None: - """Test an error is raised when record creation fails.""" - - with self.assertRaises(httpx.HTTPError): - self.client.create_cluster() - - -class Retrieve(TestCase): - """Test record retrieval via the `retrieve_cluster` method.""" - - def setUp(self) -> None: - """Create records for testing.""" - - self.client = KeystoneClient(API_HOST) - self.client.login(API_USER, API_PASSWORD) - - self._clear_old_records() - - self.test_cluster = self.client.create_cluster({ - 'name': 'Test-Cluster', - 'description': 'Cluster created for retrieval testing purposes.' - }) - - self.other_cluster = self.client.create_cluster({ - 'name': 'Other-Cluster', - 'description': 'Another cluster created for testing purposes.' - }) - - def tearDown(self) -> None: - """Delete any test records.""" - - self._clear_old_records() - self.client.close() - - def _clear_old_records(self) -> None: - """Delete any test records.""" - - cluster_list = self.client.http_get( - f'allocations/clusters/', params={'name__in': 'Test-Cluster,Other-Cluster'}) - - for cluster in cluster_list.json(): - delete = self.client.http_delete(f"allocations/clusters/{cluster['id']}/") - delete.raise_for_status() - - def test_retrieve_by_pk(self) -> None: - """Test the retrieval of a specific record via its primary key.""" - - pk = self.test_cluster['id'] - retrieved_cluster = self.client.retrieve_cluster(pk=pk) - self.assertIsNotNone(retrieved_cluster) - self.assertEqual(retrieved_cluster['id'], pk) - - def test_retrieve_with_filters(self) -> None: - """Test the filtering of returned records via search params.""" - - retrieved_clusters = self.client.retrieve_cluster(filters={"name": "Test-Cluster"}) - self.assertIsInstance(retrieved_clusters, list) - self.assertTrue(all(cluster['name'] == "Test-Cluster" for cluster in retrieved_clusters)) - - def test_retrieve_all(self) -> None: - """Test the retrieval of all records.""" - - all_clusters = self.client.retrieve_cluster() - self.assertIsInstance(all_clusters, list) - self.assertGreater(len(all_clusters), 0) - - def test_none_on_missing_record(self) -> None: - """Test `None` is returned when a record does not exist.""" - - missing_cluster = self.client.retrieve_cluster(pk=999999) - self.assertIsNone(missing_cluster) - - def test_error_on_failure(self) -> None: - """Test an error is raised when record retrieval fails.""" - - # Use an unauthenticated client session on an endpoint requiring authentication - with self.assertRaises(httpx.HTTPError), KeystoneClient(API_HOST) as client: - client.retrieve_cluster() - - -class Update(TestCase): - """Test record updates via the `update_cluster` method.""" - - def setUp(self) -> None: - """Create records for testing.""" - - self.client = KeystoneClient(API_HOST) - self.client.login(API_USER, API_PASSWORD) - - self._clear_old_records() - self.test_cluster = self.client.create_cluster({ - 'name': 'Test-Cluster', - 'description': 'Cluster created for update testing purposes.' - }) - - def tearDown(self) -> None: - """Delete any test records.""" - - self._clear_old_records() - self.client.close() - - def _clear_old_records(self) -> None: - """Delete any test records.""" - - cluster_list = self.client.http_get(f'allocations/clusters/', params={'name': 'Test-Cluster'}) - for cluster in cluster_list.json(): - delete = self.client.http_delete(f"allocations/clusters/{cluster['id']}/").raise_for_status() - delete.raise_for_status() - - def test_update_record(self) -> None: - """Test the record is updated successfully.""" - - pk = self.test_cluster['id'] - updated_data = {'description': "Updated description"} - updated_record = self.client.update_cluster(pk=pk, data=updated_data) - self.assertIsNotNone(updated_record) - self.assertEqual(updated_record['description'], "Updated description") - - def test_update_nonexistent_record(self) -> None: - """Test updating a nonexistent record raises an error.""" - - with self.assertRaises(httpx.HTTPError): - self.client.update_cluster(pk=999999, data={'description': "This should fail"}) - - def test_partial_update(self) -> None: - """Test a partial update modifies only specified fields.""" - - pk = self.test_cluster['id'] - original_status = self.test_cluster['enabled'] - - updated_data = {'description': "Partially updated description"} - updated_record = self.client.update_cluster(pk=pk, data=updated_data) - - self.assertEqual(updated_record['description'], "Partially updated description") - self.assertEqual(updated_record['enabled'], original_status) - - def test_no_update_on_empty_data(self) -> None: - """Test an empty update request does not change the record.""" - - pk = self.test_cluster['id'] - original_data = self.test_cluster.copy() - updated_record = self.client.update_cluster(pk=pk, data={}) - self.assertEqual(updated_record, original_data) - - -class Delete(TestCase): - """Test record deletion using the `delete_cluster` method.""" - - def setUp(self) -> None: - """Create records for testing.""" - - self.client = KeystoneClient(API_HOST) - self.client.login(API_USER, API_PASSWORD) - - self._clear_old_records() - self.test_cluster = self.client.create_cluster({ - 'name': 'Test-Cluster', - 'description': 'Cluster created for delete testing purposes.' - }) - - def tearDown(self) -> None: - """Delete any test records.""" - - self._clear_old_records() - self.client.close() - - def _clear_old_records(self) -> None: - """Delete any test records.""" - - cluster_list = self.client.http_get(f'allocations/clusters/', params={'name': 'Test-Cluster'}) - for cluster in cluster_list.json(): - response = self.client.http_delete(f"allocations/clusters/{cluster['id']}/") - response.raise_for_status() - - def test_delete_record(self) -> None: - """Test a record is deleted successfully.""" - - pk = self.test_cluster['id'] - self.client.delete_cluster(pk=pk) - - retrieved_cluster = self.client.retrieve_cluster(pk=pk) - self.assertIsNone(retrieved_cluster) - - def test_delete_nonexistent_record_silent(self) -> None: - """Test deleting a nonexistent record exits silently.""" - - self.client.delete_cluster(pk=999999) - - def test_delete_nonexistent_record_raise(self) -> None: - """Test deleting a nonexistent record raises an exception when specified.""" - - with self.assertRaises(httpx.HTTPError): - self.client.delete_cluster(pk=999999, raise_not_exists=True)