diff --git a/mpt_api_client/resources/helpdesk/chat_participants.py b/mpt_api_client/resources/helpdesk/chat_participants.py new file mode 100644 index 0000000..10b3d20 --- /dev/null +++ b/mpt_api_client/resources/helpdesk/chat_participants.py @@ -0,0 +1,46 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncCreateMixin, + AsyncDeleteMixin, + AsyncUpdateMixin, + CollectionMixin, + CreateMixin, + DeleteMixin, + UpdateMixin, +) +from mpt_api_client.models import Model + + +class ChatParticipant(Model): + """Helpdesk Chat Participant resource.""" + + +class ChatParticipantsServiceConfig: + """Helpdesk Chat Participants service configuration.""" + + _endpoint = "/public/v1/helpdesk/chats/{chat_id}/participants" + _model_class = ChatParticipant + _collection_key = "data" + + +class ChatParticipantsService( + CreateMixin[ChatParticipant], + UpdateMixin[ChatParticipant], + DeleteMixin, + CollectionMixin[ChatParticipant], + Service[ChatParticipant], + ChatParticipantsServiceConfig, +): + """Helpdesk Chat Participants service.""" + + +class AsyncChatParticipantsService( + AsyncCreateMixin[ChatParticipant], + AsyncUpdateMixin[ChatParticipant], + AsyncDeleteMixin, + AsyncCollectionMixin[ChatParticipant], + AsyncService[ChatParticipant], + ChatParticipantsServiceConfig, +): + """Async Helpdesk Chat Participants service.""" diff --git a/mpt_api_client/resources/helpdesk/chats.py b/mpt_api_client/resources/helpdesk/chats.py index 26d416a..297794e 100644 --- a/mpt_api_client/resources/helpdesk/chats.py +++ b/mpt_api_client/resources/helpdesk/chats.py @@ -22,6 +22,10 @@ AsyncChatMessagesService, ChatMessagesService, ) +from mpt_api_client.resources.helpdesk.chat_participants import ( + AsyncChatParticipantsService, + ChatParticipantsService, +) class Chat(Model): @@ -62,6 +66,12 @@ def links(self, chat_id: str) -> ChatLinksService: """Return chat links service.""" return ChatLinksService(http_client=self.http_client, endpoint_params={"chat_id": chat_id}) + def participants(self, chat_id: str) -> ChatParticipantsService: + """Return chat participants service.""" + return ChatParticipantsService( + http_client=self.http_client, endpoint_params={"chat_id": chat_id} + ) + class AsyncChatsService( AsyncCreateMixin[Chat], @@ -90,3 +100,9 @@ def links(self, chat_id: str) -> AsyncChatLinksService: return AsyncChatLinksService( http_client=self.http_client, endpoint_params={"chat_id": chat_id} ) + + def participants(self, chat_id: str) -> AsyncChatParticipantsService: + """Return async chat participants service.""" + return AsyncChatParticipantsService( + http_client=self.http_client, endpoint_params={"chat_id": chat_id} + ) diff --git a/pyproject.toml b/pyproject.toml index bb278f4..7da94e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,51 +5,51 @@ description = "SoftwareOne Marketplace API Client for Python" authors = [{ name = "SoftwareOne AG" }] requires-python = ">=3.12,<4" readme = "docs/PROJECT_DESCRIPTION.md" -license = {text = "Apache-2.0 license"} +license = { text = "Apache-2.0 license" } keywords = [ - "openapi", - "client", - "softwareone", - "marketplace", + "openapi", + "client", + "softwareone", + "marketplace", ] classifiers = [ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3.12", - "Topic :: Utilities", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3.12", + "Topic :: Utilities", ] dependencies = [ - "httpx==0.28.*", + "httpx==0.28.*", ] [dependency-groups] dev = [ - "dependency-injector==4.48.*", - "flake8==7.3.*", # force flake8 version to have same formatting everywhere, also update in pre-commit config - "flake8-aaa==0.17.*", # also update pre-commit config - "flake8-pyproject==1.2.*", # also update pre-commit config - "freezegun==1.5.*", - "ipdb==0.13.*", - "ipython==9.*", - "mypy==1.19.*", - "pre-commit==4.5.*", - "pyfakefs==6.1.*", - "pytest==9.0.*", - "pytest-asyncio==1.3.*", - "pytest-cov==7.0.*", - "pytest-deadfixtures==3.1.*", - "pytest-mock==3.15.*", - "pytest-randomly==4.0.*", - "pytest-reportportal==5.6.*", - "pytest-rerunfailures==16.1.*", - "pytest-xdist==3.8.*", - "responses==0.26.*", - "respx==0.22.*", - "ruff==0.15.*", # force ruff version to have same formatting everywhere - "typing-extensions==4.15.*", - "wemake-python-styleguide==1.6.*", - "types-python-dateutil==2.9.*", + "dependency-injector==4.48.*", + "flake8==7.3.*", # force flake8 version to have same formatting everywhere, also update in pre-commit config + "flake8-aaa==0.17.*", # also update pre-commit config + "flake8-pyproject==1.2.*", # also update pre-commit config + "freezegun==1.5.*", + "ipdb==0.13.*", + "ipython==9.*", + "mypy==1.19.*", + "pre-commit==4.5.*", + "pyfakefs==6.1.*", + "pytest==9.0.*", + "pytest-asyncio==1.3.*", + "pytest-cov==7.0.*", + "pytest-deadfixtures==3.1.*", + "pytest-mock==3.15.*", + "pytest-randomly==4.0.*", + "pytest-reportportal==5.6.*", + "pytest-rerunfailures==16.1.*", + "pytest-xdist==3.8.*", + "responses==0.26.*", + "respx==0.22.*", + "ruff==0.15.*", # force ruff version to have same formatting everywhere + "typing-extensions==4.15.*", + "wemake-python-styleguide==1.6.*", + "types-python-dateutil==2.9.*", ] [tool.hatch.build.targets.sdist] @@ -70,8 +70,8 @@ log_cli = false asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" filterwarnings = [ - "ignore:Support for class-based `config` is deprecated:DeprecationWarning", - "ignore:pkg_resources is deprecated as an API:DeprecationWarning", + "ignore:Support for class-based `config` is deprecated:DeprecationWarning", + "ignore:pkg_resources is deprecated as an API:DeprecationWarning", ] rp_project = "mpt-api-python-client" markers = [ @@ -85,11 +85,11 @@ source = ["mpt_api_client"] [tool.coverage.report] exclude_also = [ - "if __name__ == \"__main__\":", - "raise NotImplementedError", + "if __name__ == \"__main__\":", + "raise NotImplementedError", ] omit = [ - "*/__init__.py" + "*/__init__.py" ] [tool.flake8] @@ -122,6 +122,7 @@ per-file-ignores = [ "mpt_api_client/resources/catalog/*.py: WPS110 WPS214 WPS215 WPS235", "mpt_api_client/resources/catalog/products.py: WPS204 WPS214 WPS215 WPS235", "mpt_api_client/resources/commerce/*.py: WPS235 WPS215", + "mpt_api_client/resources/helpdesk/*.py: WPS204 WPS215", "mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214", "tests/e2e/accounts/*.py: WPS430 WPS202", "tests/e2e/billing/*.py: WPS202 WPS421 WPS118", @@ -157,48 +158,48 @@ docstring-code-format = false [tool.ruff.lint] select = [ - "A", # flake8-builtins - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "C90", # maccabe - "COM", # flake8-commas - "D", # pydocstyle - "DTZ", # flake8-datetimez - "E", # pycodestyle - "ERA", # flake8-eradicate - "EXE", # flake8-executable - "F", # pyflakes - "FBT", # flake8-boolean-trap - "FLY", # pyflint + "A", # flake8-builtins + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "C90", # maccabe + "COM", # flake8-commas + "D", # pydocstyle + "DTZ", # flake8-datetimez + "E", # pycodestyle + "ERA", # flake8-eradicate + "EXE", # flake8-executable + "F", # pyflakes + "FBT", # flake8-boolean-trap + "FLY", # pyflint "FURB", # refurb - "G", # flake8-logging-format - "I", # isort - "ICN", # flake8-import-conventions - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "N", # pep8-naming + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "N", # pep8-naming "PERF", # perflint - "PIE", # flake8-pie - "PL", # pylint - "PT", # flake8-pytest-style - "PTH", # flake8-use-pathlib - "Q", # flake8-quotes - "RET", # flake8-return - "RSE", # flake8-raise - "RUF", # ruff - "S", # flake8-bandit - "SIM", # flake8-simpify - "SLF", # flake8-self + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # ruff + "S", # flake8-bandit + "SIM", # flake8-simpify + "SLF", # flake8-self "SLOT", # flake8-slots "T100", # flake8-debugger - "TRY", # tryceratops - "UP", # pyupgrade - "W", # pycodestyle - "YTT", # flake8-2020 + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 ] ignore = [ - "A005", # allow to shadow stdlib and builtin module names - "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + "A005", # allow to shadow stdlib and builtin module names + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling "COM812", # trailing comma, conflicts with `ruff format` # Different doc rules that we don't really care about: "D100", @@ -211,15 +212,15 @@ ignore = [ "D401", "D404", "D405", - "ISC001", # implicit string concat conflicts with `ruff format` - "ISC003", # prefer explicit string concat over implicit concat - "PLR09", # we have our own complexity rules + "ISC001", # implicit string concat conflicts with `ruff format` + "ISC003", # prefer explicit string concat over implicit concat + "PLR09", # we have our own complexity rules "PLR2004", # do not report magic numbers "PLR6301", # do not require classmethod / staticmethod when self not used "PT011", # pytest.raises({exception}) is too broad, set the match parameter or use a more specific exception - "TRY003", # long exception messages from `tryceratops` + "TRY003", # long exception messages from `tryceratops` ] -external = [ "AAA", "WPS" ] +external = ["AAA", "WPS"] # Plugin configs: [tool.ruff.lint.flake8-import-conventions] diff --git a/tests/e2e/helpdesk/chats/participants/__init__.py b/tests/e2e/helpdesk/chats/participants/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/e2e/helpdesk/chats/participants/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/e2e/helpdesk/chats/participants/conftest.py b/tests/e2e/helpdesk/chats/participants/conftest.py new file mode 100644 index 0000000..a3c191c --- /dev/null +++ b/tests/e2e/helpdesk/chats/participants/conftest.py @@ -0,0 +1,45 @@ +import pytest + +from tests.e2e.helper import ( + async_create_fixture_resource_and_delete, + create_fixture_resource_and_delete, +) + + +@pytest.fixture +def chat_participants_service(mpt_ops, chat_id): + return mpt_ops.helpdesk.chats.participants(chat_id) + + +@pytest.fixture +def async_chat_participants_service(async_mpt_ops, chat_id): + return async_mpt_ops.helpdesk.chats.participants(chat_id) + + +@pytest.fixture +def chat_participant_data(account_id, user_id): + return { + "identity": {"id": user_id}, + "account": {"id": account_id}, + } + + +@pytest.fixture +def created_chat_participant(chat_participants_service, chat_participant_data): + with create_fixture_resource_and_delete( + chat_participants_service, chat_participant_data + ) as chat_participant: + yield chat_participant + + +@pytest.fixture +async def async_created_chat_participant(async_chat_participants_service, chat_participant_data): + async with async_create_fixture_resource_and_delete( + async_chat_participants_service, chat_participant_data + ) as chat_participant: + yield chat_participant + + +@pytest.fixture +def invalid_chat_participant_id(): + return "CHP-0000-0000-0000" diff --git a/tests/e2e/helpdesk/chats/participants/test_async_participants.py b/tests/e2e/helpdesk/chats/participants/test_async_participants.py new file mode 100644 index 0000000..1e7565a --- /dev/null +++ b/tests/e2e/helpdesk/chats/participants/test_async_participants.py @@ -0,0 +1,57 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError + +pytestmark = [pytest.mark.flaky] + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_list_chat_participants(async_chat_participants_service): + result = await async_chat_participants_service.fetch_page(limit=1) + + assert len(result) > 0 + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_create_chat_participant(async_created_chat_participant): # noqa: AAA01 + assert async_created_chat_participant.id is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_update_chat_participant( + async_chat_participants_service, async_created_chat_participant +): + result = await async_chat_participants_service.update( + async_created_chat_participant.id, + {"status": "Active"}, + ) + + assert result.id == async_created_chat_participant.id + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_delete_chat_participant( + async_chat_participants_service, async_created_chat_participant +): + result = async_created_chat_participant + + await async_chat_participants_service.delete(result.id) + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_update_chat_participant_not_found( + async_chat_participants_service, invalid_chat_participant_id +): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_chat_participants_service.update( + invalid_chat_participant_id, + {"status": "Active"}, + ) + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +async def test_delete_chat_participant_not_found( + async_chat_participants_service, invalid_chat_participant_id +): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_chat_participants_service.delete(invalid_chat_participant_id) diff --git a/tests/e2e/helpdesk/chats/participants/test_sync_participants.py b/tests/e2e/helpdesk/chats/participants/test_sync_participants.py new file mode 100644 index 0000000..202cfde --- /dev/null +++ b/tests/e2e/helpdesk/chats/participants/test_sync_participants.py @@ -0,0 +1,43 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError + +pytestmark = [pytest.mark.flaky] + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_list_chat_participants(chat_participants_service): + result = chat_participants_service.fetch_page(limit=1) + + assert len(result) > 0 + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_create_chat_participant(created_chat_participant): # noqa: AAA01 + assert created_chat_participant.id is not None + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_update_chat_participant(chat_participants_service, created_chat_participant): + result = chat_participants_service.update(created_chat_participant.id, {"status": "Active"}) + + assert result.id == created_chat_participant.id + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_delete_chat_participant(chat_participants_service, created_chat_participant): + result = created_chat_participant + + chat_participants_service.delete(result.id) + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_update_chat_participant_not_found(chat_participants_service, invalid_chat_participant_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + chat_participants_service.update(invalid_chat_participant_id, {"status": "Active"}) + + +@pytest.mark.skip(reason="Unskip after MPT-19124 completed") +def test_delete_chat_participant_not_found(chat_participants_service, invalid_chat_participant_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + chat_participants_service.delete(invalid_chat_participant_id) diff --git a/tests/unit/resources/helpdesk/test_chat_participants.py b/tests/unit/resources/helpdesk/test_chat_participants.py new file mode 100644 index 0000000..021dae9 --- /dev/null +++ b/tests/unit/resources/helpdesk/test_chat_participants.py @@ -0,0 +1,52 @@ +import pytest + +from mpt_api_client.resources.helpdesk.chat_participants import ( + AsyncChatParticipantsService, + ChatParticipantsService, +) + + +@pytest.fixture +def chat_participants_service(http_client) -> ChatParticipantsService: + return ChatParticipantsService( + http_client=http_client, endpoint_params={"chat_id": "CHT-0000-0000-0001"} + ) + + +@pytest.fixture +def async_chat_participants_service(async_http_client) -> AsyncChatParticipantsService: + return AsyncChatParticipantsService( + http_client=async_http_client, endpoint_params={"chat_id": "CHT-0000-0000-0001"} + ) + + +def test_endpoint(chat_participants_service) -> None: + result = ( + chat_participants_service.path + == "/public/v1/helpdesk/chats/CHT-0000-0000-0001/participants" + ) + + assert result is True + + +def test_async_endpoint(async_chat_participants_service) -> None: + result = ( + async_chat_participants_service.path + == "/public/v1/helpdesk/chats/CHT-0000-0000-0001/participants" + ) + + assert result is True + + +@pytest.mark.parametrize("method", ["create", "update", "delete", "iterate"]) +def test_methods_present(chat_participants_service, method: str) -> None: + result = hasattr(chat_participants_service, method) + + assert result is True + + +@pytest.mark.parametrize("method", ["create", "update", "delete", "iterate"]) +def test_async_methods_present(async_chat_participants_service, method: str) -> None: + result = hasattr(async_chat_participants_service, method) + + assert result is True diff --git a/tests/unit/resources/helpdesk/test_chats.py b/tests/unit/resources/helpdesk/test_chats.py index d967020..82dfd8e 100644 --- a/tests/unit/resources/helpdesk/test_chats.py +++ b/tests/unit/resources/helpdesk/test_chats.py @@ -12,6 +12,10 @@ AsyncChatMessagesService, ChatMessagesService, ) +from mpt_api_client.resources.helpdesk.chat_participants import ( + AsyncChatParticipantsService, + ChatParticipantsService, +) from mpt_api_client.resources.helpdesk.chats import AsyncChatsService, ChatsService @@ -50,6 +54,7 @@ def test_async_mixins_present(async_chats_service, method): [ ("messages", ChatMessagesService), ("links", ChatLinksService), + ("participants", ChatParticipantsService), ], ) def test_property_services(chats_service, service_method, expected_service_class): @@ -71,6 +76,7 @@ def test_attachments_service(chats_service): [ ("messages", AsyncChatMessagesService), ("links", AsyncChatLinksService), + ("participants", AsyncChatParticipantsService), ], ) def test_async_property_services(async_chats_service, service_method, expected_service_class):