diff --git a/mpt_api_client/resources/accounts/account.py b/mpt_api_client/resources/accounts/account.py index af3f195a..1a07972d 100644 --- a/mpt_api_client/resources/accounts/account.py +++ b/mpt_api_client/resources/accounts/account.py @@ -6,6 +6,14 @@ UpdateMixin, ) from mpt_api_client.models import Model +from mpt_api_client.resources.accounts.mixins import ( + ActivatableMixin, + AsyncActivatableMixin, + AsyncEnablableMixin, + AsyncValidateMixin, + EnablableMixin, + ValidateMixin, +) class Account(Model): @@ -23,6 +31,9 @@ class AccountsServiceConfig: class AccountsService( CreateMixin[Account], UpdateMixin[Account], + ActivatableMixin[Account], + EnablableMixin[Account], + ValidateMixin[Account], Service[Account], AccountsServiceConfig, ): @@ -32,6 +43,9 @@ class AccountsService( class AsyncAccountsService( AsyncCreateMixin[Account], AsyncUpdateMixin[Account], + AsyncActivatableMixin[Account], + AsyncEnablableMixin[Account], + AsyncValidateMixin[Account], AsyncService[Account], AccountsServiceConfig, ): diff --git a/mpt_api_client/resources/accounts/mixins.py b/mpt_api_client/resources/accounts/mixins.py new file mode 100644 index 00000000..263f4f86 --- /dev/null +++ b/mpt_api_client/resources/accounts/mixins.py @@ -0,0 +1,140 @@ +from mpt_api_client.models import ResourceData + +# TODO: Consider reorganizing functions in mixins to reduce duplication and differences amongst +# different domains + + +class ActivatableMixin[Model]: + """Activatable mixin for activating, enabling, disabling and deactivating resources.""" + + def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Activate a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "activate", json=resource_data + ) + + def deactivate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Deactivate a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "deactivate", json=resource_data + ) + + +class EnablableMixin[Model]: + """Enablable mixin for enabling and disabling resources.""" + + def enable(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Enable a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "enable", json=resource_data + ) + + def disable(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Disable a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "disable", json=resource_data + ) + + +class ValidateMixin[Model]: + """Validate mixin adds the ability to validate a resource.""" + + def validate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Validate a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be validated + """ + return self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "validate", json=resource_data + ) + + +class AsyncActivatableMixin[Model]: + """Async activatable mixin for activating, enabling, disabling and deactivating resources.""" + + async def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Activate a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return await self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "activate", json=resource_data + ) + + async def deactivate( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> Model: + """Deactivate a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return await self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "deactivate", json=resource_data + ) + + +class AsyncEnablableMixin[Model]: + """Asynchronous Enablable mixin for enabling and disabling resources.""" + + async def enable(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Enable a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return await self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "enable", json=resource_data + ) + + async def disable(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Disable a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be updated + """ + return await self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "disable", json=resource_data + ) + + +class AsyncValidateMixin[Model]: + """Asynchronous Validate mixin adds the ability to validate a resource.""" + + async def validate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model: + """Validate a resource. + + Args: + resource_id: Resource ID + resource_data: Resource data will be validated + """ + return await self._resource_action( # type: ignore[attr-defined, no-any-return] + resource_id, "POST", "validate", json=resource_data + ) diff --git a/tests/resources/accounts/test_account.py b/tests/resources/accounts/test_account.py index 678c4a3e..46f5ab1e 100644 --- a/tests/resources/accounts/test_account.py +++ b/tests/resources/accounts/test_account.py @@ -13,11 +13,15 @@ def async_account_service(async_http_client): return AsyncAccountsService(http_client=async_http_client) -@pytest.mark.parametrize("method", ["get", "create", "update"]) +@pytest.mark.parametrize( + "method", ["get", "create", "update", "enable", "disable", "activate", "deactivate", "validate"] +) def test_mixins_present(account_service, method): assert hasattr(account_service, method) -@pytest.mark.parametrize("method", ["get", "create", "update"]) +@pytest.mark.parametrize( + "method", ["get", "create", "update", "enable", "disable", "activate", "deactivate", "validate"] +) def test_async_mixins_present(async_account_service, method): assert hasattr(async_account_service, method) diff --git a/tests/resources/accounts/test_mixins.py b/tests/resources/accounts/test_mixins.py new file mode 100644 index 00000000..a20de45f --- /dev/null +++ b/tests/resources/accounts/test_mixins.py @@ -0,0 +1,452 @@ +import httpx +import pytest +import respx + +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.resources.accounts.mixins import ( + ActivatableMixin, + AsyncActivatableMixin, + AsyncEnablableMixin, + AsyncValidateMixin, + EnablableMixin, + ValidateMixin, +) +from tests.conftest import DummyModel + + +class DummyActivatableService( + ActivatableMixin[DummyModel], + Service[DummyModel], +): + _endpoint = "/public/v1/dummy/activatable/" + _model_class = DummyModel + _collection_key = "data" + + +class DummyAsyncActivatableService( + AsyncActivatableMixin[DummyModel], + AsyncService[DummyModel], +): + _endpoint = "/public/v1/dummy/activatable/" + _model_class = DummyModel + _collection_key = "data" + + +class DummyEnablableService( + EnablableMixin[DummyModel], + Service[DummyModel], +): + _endpoint = "/public/v1/dummy/enablable/" + _model_class = DummyModel + _collection_key = "data" + + +class DummyAsyncEnablableService( + AsyncEnablableMixin[DummyModel], + AsyncService[DummyModel], +): + _endpoint = "/public/v1/dummy/enablable/" + _model_class = DummyModel + _collection_key = "data" + + +class DummyValidateService( + ValidateMixin[DummyModel], + Service[DummyModel], +): + _endpoint = "/public/v1/dummy/validate/" + _model_class = DummyModel + _collection_key = "data" + + +class DummyAsyncValidateService( + AsyncValidateMixin[DummyModel], + AsyncService[DummyModel], +): + _endpoint = "/public/v1/dummy/validate/" + _model_class = DummyModel + _collection_key = "data" + + +@pytest.fixture +def activatable_service(http_client): + return DummyActivatableService(http_client=http_client) + + +@pytest.fixture +def async_activatable_service(async_http_client): + return DummyAsyncActivatableService(http_client=async_http_client) + + +@pytest.fixture +def enablable_service(http_client): + return DummyEnablableService(http_client=http_client) + + +@pytest.fixture +def async_enablable_service(async_http_client): + return DummyAsyncEnablableService(http_client=async_http_client) + + +@pytest.fixture +def validate_service(http_client): + return DummyValidateService(http_client=http_client) + + +@pytest.fixture +def async_validate_service(async_http_client): + return DummyAsyncValidateService(http_client=async_http_client) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("activate", {"id": "OBJ-0000-0001", "status": "update"}), + ("deactivate", {"id": "OBJ-0000-0001", "status": "update"}), + ], +) +def test_activatable_resource_actions(activatable_service, action, input_status): + request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}' + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/activatable/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + activatable_obj = getattr(activatable_service, action)("OBJ-0000-0001", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert activatable_obj.to_dict() == response_expected_data + assert isinstance(activatable_obj, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("activate", None), + ("deactivate", None), + ], +) +def test_activatable_resource_actions_no_data(activatable_service, action, input_status): + request_expected_content = b"" + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/activatable/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + activatable_obj = getattr(activatable_service, action)("OBJ-0000-0001", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert activatable_obj.to_dict() == response_expected_data + assert isinstance(activatable_obj, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("activate", {"id": "OBJ-0000-0001", "status": "update"}), + ("deactivate", {"id": "OBJ-0000-0001", "status": "update"}), + ], +) +async def test_async_activatable_resource_actions(async_activatable_service, action, input_status): + request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}' + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/activatable/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + activatable_obj = await getattr(async_activatable_service, action)( + "OBJ-0000-0001", input_status + ) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert activatable_obj.to_dict() == response_expected_data + assert isinstance(activatable_obj, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("activate", None), + ("deactivate", None), + ], +) +async def test_async_activatable_resource_actions_no_data( + async_activatable_service, action, input_status +): + request_expected_content = b"" + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/activatable/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + activatable_obj = await getattr(async_activatable_service, action)( + "OBJ-0000-0001", input_status + ) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert activatable_obj.to_dict() == response_expected_data + assert isinstance(activatable_obj, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("enable", {"id": "OBJ-0000-0001", "status": "update"}), + ("disable", {"id": "OBJ-0000-0001", "status": "update"}), + ], +) +def test_enablable_resource_actions(enablable_service, action, input_status): + request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}' + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/enablable/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + enablable_obj = getattr(enablable_service, action)("OBJ-0000-0001", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert enablable_obj.to_dict() == response_expected_data + assert isinstance(enablable_obj, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("enable", None), + ("disable", None), + ], +) +def test_enablable_resource_actions_no_data(enablable_service, action, input_status): + request_expected_content = b"" + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/enablable/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + enablable_obj = getattr(enablable_service, action)("OBJ-0000-0001", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert enablable_obj.to_dict() == response_expected_data + assert isinstance(enablable_obj, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("enable", {"id": "OBJ-0000-0001", "status": "update"}), + ("disable", {"id": "OBJ-0000-0001", "status": "update"}), + ], +) +async def test_async_enablable_resource_actions(async_enablable_service, action, input_status): + request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}' + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/enablable/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + enablable_obj = await getattr(async_enablable_service, action)( + "OBJ-0000-0001", input_status + ) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert enablable_obj.to_dict() == response_expected_data + assert isinstance(enablable_obj, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), + [ + ("enable", None), + ("disable", None), + ], +) +async def test_async_enablable_resource_actions_no_data( + async_enablable_service, action, input_status +): + request_expected_content = b"" + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/enablable/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + enablable_obj = await getattr(async_enablable_service, action)( + "OBJ-0000-0001", input_status + ) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert enablable_obj.to_dict() == response_expected_data + assert isinstance(enablable_obj, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), [("validate", {"id": "OBJ-0000-0001", "status": "update"})] +) +def test_validate_resource_actions(validate_service, action, input_status): + request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}' + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/validate/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + validate_obj = getattr(validate_service, action)("OBJ-0000-0001", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert validate_obj.to_dict() == response_expected_data + assert isinstance(validate_obj, DummyModel) + + +@pytest.mark.parametrize(("action", "input_status"), [("validate", None)]) +def test_validate_resource_actions_no_data(validate_service, action, input_status): + request_expected_content = b"" + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/validate/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + validate_obj = getattr(validate_service, action)("OBJ-0000-0001", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert validate_obj.to_dict() == response_expected_data + assert isinstance(validate_obj, DummyModel) + + +@pytest.mark.parametrize( + ("action", "input_status"), [("validate", {"id": "OBJ-0000-0001", "status": "update"})] +) +async def test_async_validate_resource_actions(async_validate_service, action, input_status): + request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}' + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/validate/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + validate_obj = await getattr(async_validate_service, action)("OBJ-0000-0001", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert validate_obj.to_dict() == response_expected_data + assert isinstance(validate_obj, DummyModel) + + +@pytest.mark.parametrize(("action", "input_status"), [("validate", None)]) +async def test_async_validate_resource_actions_no_data( + async_validate_service, action, input_status +): + request_expected_content = b"" + response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/dummy/validate/OBJ-0000-0001/{action}" + ).mock( + return_value=httpx.Response( + status_code=httpx.codes.OK, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + validate_obj = await getattr(async_validate_service, action)("OBJ-0000-0001", input_status) + + assert mock_route.call_count == 1 + request = mock_route.calls[0].request + + assert request.content == request_expected_content + assert validate_obj.to_dict() == response_expected_data + assert isinstance(validate_obj, DummyModel)