From 2c6f09340e614c436565231c22c26ff225d4ec59 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:03:00 +0200 Subject: [PATCH 01/27] Define base tools related to mixins --- centreon_mcp/components/base.py | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 centreon_mcp/components/base.py diff --git a/centreon_mcp/components/base.py b/centreon_mcp/components/base.py new file mode 100644 index 0000000..8214205 --- /dev/null +++ b/centreon_mcp/components/base.py @@ -0,0 +1,52 @@ +import asyncio +import json +from collections.abc import Sequence + +from pydantic import BaseModel + +from centreon_mcp.utils.base import BaseFilter, BaseOrder +from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin, PatchMixin + + +async def _list[CentreonModel: ListMixin]( + model: type[CentreonModel], + filters: Sequence[BaseFilter] | None = None, + limit: int = 10, + page: int = 1, + order: BaseOrder | None = None, +) -> list[CentreonModel]: + """ + Generic function to list ressources based on provided filters, pagination and order. + """ + search = json.dumps(BaseFilter.join(filters)) + sort_by = order.model_dump_json() if order else None + return await model.list(search, limit, page, sort_by) + + +async def _delete[CentreonModel: DeleteMixin]( + model: type[CentreonModel], model_ids: list[int] +) -> dict[int, bool | BaseException]: + """ + Generic function to delete multiple resources based on their ids. + """ + tasks = [asyncio.create_task(model.delete(model_id)) for model_id in model_ids] + results = await asyncio.gather(*tasks, return_exceptions=True) + return dict(zip(model_ids, results, strict=True)) + + +async def _create[CentreonModel: CreateMixin]( + model: type[CentreonModel], params: BaseModel +) -> bool: + """ + Generic function to create a resource based on params. + """ + return await model.create(params) + + +async def _patch[CentreonModel: PatchMixin]( + model: type[CentreonModel], model_id: int, params: BaseModel +) -> bool: + """ + Generic function to patch a resource based on params. + """ + return await model.patch(model_id, params) From c22c50f5b7c617f15763b727c9e3abfb188bb900 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:03:40 +0200 Subject: [PATCH 02/27] Use _list for acknowledgements --- centreon_mcp/components/acknowledgement.py | 5 +++-- tests/components/test_component_acknowledgement.py | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/centreon_mcp/components/acknowledgement.py b/centreon_mcp/components/acknowledgement.py index 8955deb..204e3c4 100644 --- a/centreon_mcp/components/acknowledgement.py +++ b/centreon_mcp/components/acknowledgement.py @@ -3,13 +3,14 @@ from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _list from centreon_mcp.types.acknowledgement import ( Acknowledgement, AcknowledgementParams, AcknowledgementResource, ) from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder acknowledgement = FastMCP() @@ -53,7 +54,7 @@ async def list_acknowledgements( List all acknowledgements in real-time monitoring. """ logger.info("Executing tool list_acknowledgements") - return await _list(Acknowledgement, AcknowledgementOrder, filters, limit, page, order) + return await _list(Acknowledgement, filters, limit, page, order) @acknowledgement.tool( diff --git a/tests/components/test_component_acknowledgement.py b/tests/components/test_component_acknowledgement.py index 6d20c6a..ce4fb22 100644 --- a/tests/components/test_component_acknowledgement.py +++ b/tests/components/test_component_acknowledgement.py @@ -37,9 +37,7 @@ async def test_list_acknowledgements(logger: MagicMock, _list: AsyncMock): results = await list_acknowledgements(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with( - Acknowledgement, AcknowledgementOrder, filters, limit, page, order - ) + _list.assert_awaited_once_with(Acknowledgement, filters, limit, page, order) # Assert result assert results[0] == acknowledgement From fee671995441513b635ddd979de3bc4c22d3adca Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:04:00 +0200 Subject: [PATCH 03/27] Use _list for commands --- centreon_mcp/components/command.py | 5 +++-- tests/components/test_component_command.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/centreon_mcp/components/command.py b/centreon_mcp/components/command.py index 32b7315..19110a1 100644 --- a/centreon_mcp/components/command.py +++ b/centreon_mcp/components/command.py @@ -3,9 +3,10 @@ from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _list from centreon_mcp.types.command import Command, CommandParams, CommandType from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder command = FastMCP() @@ -42,7 +43,7 @@ async def list_commands( to avoid retrieving all commands except if explicitly intended. """ logger.info("Executing tool list_commands") - return await _list(Command, CommandOrder, filters, limit, page, order) + return await _list(Command, filters, limit, page, order) @command.tool( diff --git a/tests/components/test_component_command.py b/tests/components/test_component_command.py index 8eedfd3..1a5b0f3 100644 --- a/tests/components/test_component_command.py +++ b/tests/components/test_component_command.py @@ -32,7 +32,7 @@ async def test_list_commands(logger: MagicMock, _list: AsyncMock): results = await list_commands(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with(Command, CommandOrder, filters, limit, page, order) + _list.assert_awaited_once_with(Command, filters, limit, page, order) # Assert result assert results[0] == command From e4801283cd39422ec822923a381b45420662ea5d Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:07:27 +0200 Subject: [PATCH 04/27] Use _list, _delete for downtimes --- centreon_mcp/components/downtime.py | 10 ++++------ tests/components/test_component_downtime.py | 14 +++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/centreon_mcp/components/downtime.py b/centreon_mcp/components/downtime.py index 84cb10f..866ebd0 100644 --- a/centreon_mcp/components/downtime.py +++ b/centreon_mcp/components/downtime.py @@ -1,13 +1,13 @@ -import asyncio from typing import Annotated, Literal from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _delete, _list from centreon_mcp.types.downtime import Downtime, DowntimeParams, DowntimeResource from centreon_mcp.types.host import HostState from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder downtime = FastMCP() @@ -59,7 +59,7 @@ async def list_downtimes( to avoid retrieving all downtimes except if explicitly intended. """ logger.info("Executing tool list_downtimes") - return await _list(Downtime, DowntimeOrder, filters, limit, page, order) + return await _list(Downtime, filters, limit, page, order) @downtime.tool( @@ -95,6 +95,4 @@ async def cancel_downtimes(downtime_ids: list[int]) -> dict[int, bool | BaseExce Use tools `list_downtimes` first to get downtime IDs. """ logger.info("Executing tool cancel_downtimes") - tasks = [asyncio.create_task(Downtime.delete(downtime_id)) for downtime_id in downtime_ids] - results = await asyncio.gather(*tasks, return_exceptions=True) - return dict(zip(downtime_ids, results, strict=True)) + return await _delete(Downtime, downtime_ids) diff --git a/tests/components/test_component_downtime.py b/tests/components/test_component_downtime.py index 949d7ee..1880515 100644 --- a/tests/components/test_component_downtime.py +++ b/tests/components/test_component_downtime.py @@ -33,7 +33,7 @@ async def test_list_downtimes(logger: MagicMock, _list: AsyncMock): results = await list_downtimes(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with(Downtime, DowntimeOrder, filters, limit, page, order) + _list.assert_awaited_once_with(Downtime, filters, limit, page, order) # Assert result assert results[0] == downtime @@ -63,9 +63,9 @@ async def test_set_downtime(logger: MagicMock, downtime_set: AsyncMock): assert result -@patch(f"{MODULE}.Downtime.delete", new_callable=AsyncMock) +@patch(f"{MODULE}._delete", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_cancel_downtimes(logger: MagicMock, downtime_delete: AsyncMock): +async def test_cancel_downtimes(logger: MagicMock, _delete: AsyncMock): # Setup args downtime_id = 10 @@ -73,14 +73,14 @@ async def test_cancel_downtimes(logger: MagicMock, downtime_delete: AsyncMock): # Mock logger logger.info.return_value = None - # Mock Downtime.delete - downtime_delete.return_value = True + # Mock _delete + _delete.return_value = {downtime_id: True} # Call test function results = await cancel_downtimes([downtime_id]) - # Assert Downtime.delete called with right args - downtime_delete.assert_awaited_once_with(downtime_id) + # Assert _delete called with right args + _delete.assert_awaited_once_with(Downtime, [downtime_id]) # Assert result assert results == {downtime_id: True} From 87a7b9874f34e2ec0797bcb1021645fe2157ff99 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:07:55 +0200 Subject: [PATCH 05/27] Use _list for monitoring servers --- centreon_mcp/components/monitoring_server.py | 5 +++-- tests/components/test_component_monitoring_server.py | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/centreon_mcp/components/monitoring_server.py b/centreon_mcp/components/monitoring_server.py index f7443bd..cb57a79 100644 --- a/centreon_mcp/components/monitoring_server.py +++ b/centreon_mcp/components/monitoring_server.py @@ -3,9 +3,10 @@ from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _list from centreon_mcp.types.monitoring_server import MonitoringServer from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder monitoring_server = FastMCP() @@ -41,4 +42,4 @@ async def list_monitoring_servers( to avoid retrieving all monitoring servers except if explicitly intended. """ logger.info("Executing tool list_monitoring_servers") - return await _list(MonitoringServer, MonitoringServerOrder, filters, limit, page, order) + return await _list(MonitoringServer, filters, limit, page, order) diff --git a/tests/components/test_component_monitoring_server.py b/tests/components/test_component_monitoring_server.py index 52e8aac..c418ad9 100644 --- a/tests/components/test_component_monitoring_server.py +++ b/tests/components/test_component_monitoring_server.py @@ -31,9 +31,7 @@ async def test_list_monitoring_servers(logger: MagicMock, _list: AsyncMock): results = await list_monitoring_servers(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with( - MonitoringServer, MonitoringServerOrder, filters, limit, page, order - ) + _list.assert_awaited_once_with(MonitoringServer, filters, limit, page, order) # Assert result assert results[0] == monitoring_server From 434d0c5b24a98107c5bd2f3573b7c120c7411641 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:09:30 +0200 Subject: [PATCH 06/27] Use _list, _create, _delete for host categories --- centreon_mcp/components/host_category.py | 17 +++------- .../test_component_host_category.py | 32 ++++++++----------- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/centreon_mcp/components/host_category.py b/centreon_mcp/components/host_category.py index 5cfaaf9..cfdc23b 100644 --- a/centreon_mcp/components/host_category.py +++ b/centreon_mcp/components/host_category.py @@ -1,16 +1,16 @@ -import asyncio from typing import Annotated, Literal from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _create, _delete, _list from centreon_mcp.types.host_category import ( HostCategoryConfiguration, HostCategoryConfigurationFullParams, HostCategoryConfigurationPartialParams, ) from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder host_category = FastMCP() @@ -47,9 +47,7 @@ async def list_host_category_configurations( to avoid retrieving all host categories except if explicitly intended. """ logger.info("Executing tool list_host_category_configurations") - return await _list( - HostCategoryConfiguration, HostCategoryConfigurationOrder, filters, limit, page, order - ) + return await _list(HostCategoryConfiguration, filters, limit, page, order) @host_category.tool( @@ -66,7 +64,7 @@ async def create_host_category_configuration(params: HostCategoryConfigurationFu Create a host category configuration. """ logger.info("Executing tool create_host_category_configuration") - return await HostCategoryConfiguration.create(params) + return await _create(HostCategoryConfiguration, params) @host_category.tool( @@ -109,9 +107,4 @@ async def delete_host_category_configurations( Delete multiple host category configurations. """ logger.info("Executing tool delete_host_category_configurations") - tasks = [ - asyncio.create_task(HostCategoryConfiguration.delete(host_category_id)) - for host_category_id in host_category_ids - ] - results = await asyncio.gather(*tasks, return_exceptions=True) - return dict(zip(host_category_ids, results, strict=True)) + return await _delete(HostCategoryConfiguration, host_category_ids) diff --git a/tests/components/test_component_host_category.py b/tests/components/test_component_host_category.py index 465133d..90b3230 100644 --- a/tests/components/test_component_host_category.py +++ b/tests/components/test_component_host_category.py @@ -38,19 +38,15 @@ async def test_list_host_category_configurations(logger: MagicMock, _list: Async results = await list_host_category_configurations(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with( - HostCategoryConfiguration, HostCategoryConfigurationOrder, filters, limit, page, order - ) + _list.assert_awaited_once_with(HostCategoryConfiguration, filters, limit, page, order) # Assert result assert results[0] == host_category_configuration -@patch(f"{MODULE}.HostCategoryConfiguration.create", new_callable=AsyncMock) +@patch(f"{MODULE}._create", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_create_host_category_configuration( - logger: MagicMock, host_category_configuration_create: AsyncMock -): +async def test_create_host_category_configuration(logger: MagicMock, _create: AsyncMock): # Setup args params = HostCategoryConfigurationFullParams.model_construct() @@ -58,14 +54,14 @@ async def test_create_host_category_configuration( # Mock logger logger.info.return_value = None - # Mock HostCategoryConfiguration.create - host_category_configuration_create.return_value = True + # Mock _create + _create.return_value = True # Call test function result = await create_host_category_configuration(params) - # Assert HostCategoryConfiguration.create called with right args - host_category_configuration_create.assert_awaited_once_with(params) + # Assert _create called with right args + _create.assert_awaited_once_with(HostCategoryConfiguration, params) # Assert result assert result @@ -112,11 +108,9 @@ async def test_update_host_category_configuration( assert result -@patch(f"{MODULE}.HostCategoryConfiguration.delete", new_callable=AsyncMock) +@patch(f"{MODULE}._delete", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_delete_host_category_configurations( - logger: MagicMock, host_category_configuration_delete: AsyncMock -): +async def test_delete_host_category_configurations(logger: MagicMock, _delete: AsyncMock): # Setup args host_category_id = 10 @@ -124,14 +118,14 @@ async def test_delete_host_category_configurations( # Mock logger logger.info.return_value = None - # Mock HostCategoryConfiguration.delete - host_category_configuration_delete.return_value = True + # Mock _delete + _delete.return_value = {host_category_id: True} # Call test function result = await delete_host_category_configurations([host_category_id]) - # Assert HostConfigurationCategory.delete called with right args - host_category_configuration_delete.assert_awaited_once_with(host_category_id) + # Assert _delete called with right args + _delete.assert_awaited_once_with(HostCategoryConfiguration, [host_category_id]) # Assert result assert result == {host_category_id: True} From 85480bdd5f35ed0ba51a27dc04ac8580ab6bee0f Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:10:04 +0200 Subject: [PATCH 07/27] Use _list, _create, _delete for host groups --- centreon_mcp/components/host_group.py | 19 ++++------- tests/components/test_component_host_group.py | 34 ++++++++----------- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/centreon_mcp/components/host_group.py b/centreon_mcp/components/host_group.py index 3db764d..9eea6a4 100644 --- a/centreon_mcp/components/host_group.py +++ b/centreon_mcp/components/host_group.py @@ -1,9 +1,9 @@ -import asyncio from typing import Annotated, Literal from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _create, _delete, _list from centreon_mcp.types.host import HostState from centreon_mcp.types.host_group import ( HostGroup, @@ -12,7 +12,7 @@ HostGroupConfigurationPartialParams, ) from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder host_group = FastMCP() @@ -53,7 +53,7 @@ async def list_host_groups( to avoid retrieving all host groups except if explicitly intended. """ logger.info("Executing tool list_host_groups") - return await _list(HostGroup, HostGroupOrder, filters, limit, page, order) + return await _list(HostGroup, filters, limit, page, order) class HostGroupConfigurationOrder(BaseOrder): @@ -88,9 +88,7 @@ async def list_host_group_configurations( to avoid retrieving all host groups except if explicitly intended. """ logger.info("Executing tool list_hostgroup_configurations") - return await _list( - HostGroupConfiguration, HostGroupConfigurationOrder, filters, limit, page, order - ) + return await _list(HostGroupConfiguration, filters, limit, page, order) @host_group.tool( @@ -107,7 +105,7 @@ async def create_host_group_configuration(params: HostGroupConfigurationFullPara Create a hostgroup. """ logger.info("Executing tool create_hostgroup_configuration") - return await HostGroupConfiguration.create(params) + return await _create(HostGroupConfiguration, params) @host_group.tool( @@ -153,9 +151,4 @@ async def delete_host_group_configurations( Delete multiple host group configurations. """ logger.info("Executing tool delete_host_group_configurations") - tasks = [ - asyncio.create_task(HostGroupConfiguration.delete(hostgroup_id)) - for hostgroup_id in hostgroup_ids - ] - results = await asyncio.gather(*tasks, return_exceptions=True) - return dict(zip(hostgroup_ids, results, strict=True)) + return await _delete(HostGroupConfiguration, hostgroup_ids) diff --git a/tests/components/test_component_host_group.py b/tests/components/test_component_host_group.py index 3a8503f..81ffe0f 100644 --- a/tests/components/test_component_host_group.py +++ b/tests/components/test_component_host_group.py @@ -44,7 +44,7 @@ async def test_list_resources(logger: MagicMock, _list: AsyncMock): results = await list_host_groups(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with(HostGroup, HostGroupOrder, filters, limit, page, order) + _list.assert_awaited_once_with(HostGroup, filters, limit, page, order) # Assert result assert results[0] == hostgroup @@ -71,19 +71,15 @@ async def test_list_host_group_configurations(logger: MagicMock, _list: AsyncMoc results = await list_host_group_configurations(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with( - HostGroupConfiguration, HostGroupConfigurationOrder, filters, limit, page, order - ) + _list.assert_awaited_once_with(HostGroupConfiguration, filters, limit, page, order) # Assert result assert results[0] == hostgroup_configuration -@patch(f"{MODULE}.HostGroupConfiguration.create", new_callable=AsyncMock) +@patch(f"{MODULE}._create", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_create_host_group_configuration( - logger: MagicMock, hostgroup_configuration_create: AsyncMock -): +async def test_create_host_group_configuration(logger: MagicMock, _create: AsyncMock): # Setup args params = HostGroupConfigurationFullParams.model_construct() @@ -91,14 +87,14 @@ async def test_create_host_group_configuration( # Mock logger logger.info.return_value = None - # Mock HostGroupConfiguration.add - hostgroup_configuration_create.return_value = True + # Mock _create + _create.return_value = True # Call test function result = await create_host_group_configuration(params) - # Assert HostqgqroupConfiguration.add called with right args - hostgroup_configuration_create.assert_awaited_once_with(params) + # Assert _create called with right args + _create.assert_awaited_once_with(HostGroupConfiguration, params) # Assert result assert result @@ -152,11 +148,9 @@ async def test_update_host_group_configuration( assert result -@patch(f"{MODULE}.HostGroupConfiguration.delete", new_callable=AsyncMock) +@patch(f"{MODULE}._delete", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_delete_host_group_configurations( - logger: MagicMock, hostgroup_configuration_delete: AsyncMock -): +async def test_delete_host_group_configurations(logger: MagicMock, _delete: AsyncMock): # Setup args hostgroup_id = 10 @@ -164,14 +158,14 @@ async def test_delete_host_group_configurations( # Mock logger logger.info.return_value = None - # Mock HostGroupConfiguration.delete - hostgroup_configuration_delete.return_value = True + # Mock _delete + _delete.return_value = {hostgroup_id: True} # Call test function result = await delete_host_group_configurations([hostgroup_id]) - # Assert HostConfigurationGroup.delete called with right args - hostgroup_configuration_delete.assert_awaited_once_with(hostgroup_id) + # Assert _delete called with right args + _delete.assert_awaited_once_with(HostGroupConfiguration, [hostgroup_id]) # Assert result assert result == {hostgroup_id: True} From 0075a4ff6df9e5fcbc1da6d108fbf0664d6ffbba Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:10:37 +0200 Subject: [PATCH 08/27] Use _list, _create, _delete for host severities --- centreon_mcp/components/host_severity.py | 15 ++++------- .../test_component_host_severity.py | 26 +++++++++---------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/centreon_mcp/components/host_severity.py b/centreon_mcp/components/host_severity.py index cd3a2c8..6131928 100644 --- a/centreon_mcp/components/host_severity.py +++ b/centreon_mcp/components/host_severity.py @@ -1,16 +1,16 @@ -import asyncio from typing import Annotated, Literal from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _create, _delete, _list from centreon_mcp.types.host_severity import ( HostSeverity, HostSeverityFullParams, HostSeverityPartialParams, ) from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder host_severity = FastMCP() @@ -49,7 +49,7 @@ async def list_host_severities( to avoid retrieving all host severities except if explicitly intended. """ logger.info("Executing tool list_host_severities") - return await _list(HostSeverity, HostSeverityOrder, filters, limit, page, order) + return await _list(HostSeverity, filters, limit, page, order) @host_severity.tool( @@ -66,7 +66,7 @@ async def create_host_severity(params: HostSeverityFullParams) -> bool: Create a host severity from params. """ logger.info("Executing tool create_host_severity") - return await HostSeverity.create(params) + return await _create(HostSeverity, params) @host_severity.tool( @@ -103,9 +103,4 @@ async def delete_host_severities(host_severity_ids: list[int]) -> dict[int, bool Use tools `list_host_severities` first to get host severities IDs. """ logger.info("Executing tool delete_host_severities") - tasks = [ - asyncio.create_task(HostSeverity.delete(host_severity_id)) - for host_severity_id in host_severity_ids - ] - results = await asyncio.gather(*tasks, return_exceptions=True) - return dict(zip(host_severity_ids, results, strict=True)) + return await _delete(HostSeverity, host_severity_ids) diff --git a/tests/components/test_component_host_severity.py b/tests/components/test_component_host_severity.py index 04fce33..9dc2913 100644 --- a/tests/components/test_component_host_severity.py +++ b/tests/components/test_component_host_severity.py @@ -38,15 +38,15 @@ async def test_list_host_severities(logger: MagicMock, _list: AsyncMock): results = await list_host_severities(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with(HostSeverity, HostSeverityOrder, filters, limit, page, order) + _list.assert_awaited_once_with(HostSeverity, filters, limit, page, order) # Assert result assert results[0] == host_severity -@patch(f"{MODULE}.HostSeverity.create", new_callable=AsyncMock) +@patch(f"{MODULE}._create", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_create_host_severity(logger: MagicMock, host_severity_create: AsyncMock): +async def test_create_host_severity(logger: MagicMock, _create: AsyncMock): # Setup args params = HostSeverityFullParams.model_construct() @@ -54,14 +54,14 @@ async def test_create_host_severity(logger: MagicMock, host_severity_create: Asy # Mock logger logger.info.return_value = None - # Mock HostSeverity.create - host_severity_create.return_value = True + # Mock _create + _create.return_value = True # Call test function result = await create_host_severity(params) - # Assert HostSeverity.create called with right args - host_severity_create.assert_awaited_once_with(params) + # Assert _create called with right args + _create.assert_awaited_once_with(HostSeverity, params) # Assert result assert result @@ -102,9 +102,9 @@ async def test_update_host_severity( assert result -@patch(f"{MODULE}.HostSeverity.delete", new_callable=AsyncMock) +@patch(f"{MODULE}._delete", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_delete_host_severities(logger: MagicMock, host_severity_delete: AsyncMock): +async def test_delete_host_severities(logger: MagicMock, _delete: AsyncMock): # Setup args host_severity_id = 10 @@ -112,14 +112,14 @@ async def test_delete_host_severities(logger: MagicMock, host_severity_delete: A # Mock logger logger.info.return_value = None - # Mock HostSeverity.delete - host_severity_delete.return_value = True + # Mock _delete + _delete.return_value = {host_severity_id: True} # Call test function result = await delete_host_severities([host_severity_id]) - # Assert HostSeverity.delete called with right args - host_severity_delete.assert_awaited_once_with(host_severity_id) + # Assert _delete called with right args + _delete.assert_awaited_once_with(HostSeverity, [host_severity_id]) # Assert result assert result == {host_severity_id: True} From fbc5108d483356f3896ea0ff0e3adfdd729937b6 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:12:05 +0200 Subject: [PATCH 09/27] Use _list, _create, _delete, _patch for host templates --- centreon_mcp/components/host_template.py | 17 +++------ .../test_component_host_template.py | 38 +++++++++---------- 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/centreon_mcp/components/host_template.py b/centreon_mcp/components/host_template.py index d6353f3..654a7b0 100644 --- a/centreon_mcp/components/host_template.py +++ b/centreon_mcp/components/host_template.py @@ -1,16 +1,16 @@ -import asyncio from typing import Annotated, Literal from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _create, _delete, _list, _patch from centreon_mcp.types.host_template import ( HostTemplate, HostTemplateFullParams, HostTemplatePartialParams, ) from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder host_template = FastMCP() @@ -47,7 +47,7 @@ async def list_host_templates( to avoid retrieving all host templates except if explicitly intended. """ logger.info("Executing tool list_host_templates") - return await _list(HostTemplate, HostTemplateOrder, filters, limit, page, order) + return await _list(HostTemplate, filters, limit, page, order) @host_template.tool( @@ -64,7 +64,7 @@ async def create_host_template(params: HostTemplateFullParams) -> bool: Create a host template from params. """ logger.info("Executing tool create_host_template") - return await HostTemplate.create(params) + return await _create(HostTemplate, params) @host_template.tool( @@ -81,7 +81,7 @@ async def update_host_template(host_template_id: int, params: HostTemplatePartia Update a host template from params. """ logger.info("Executing tool update_host_template") - return await HostTemplate.patch(host_template_id, params) + return await _patch(HostTemplate, host_template_id, params) @host_template.tool( @@ -98,9 +98,4 @@ async def delete_host_templates(host_template_ids: list[int]) -> dict[int, bool Delete multiple host templates. """ logger.info("Executing tool delete_host_templates") - tasks = [ - asyncio.create_task(HostTemplate.delete(host_template_id)) - for host_template_id in host_template_ids - ] - results = await asyncio.gather(*tasks, return_exceptions=True) - return dict(zip(host_template_ids, results, strict=True)) + return await _delete(HostTemplate, host_template_ids) diff --git a/tests/components/test_component_host_template.py b/tests/components/test_component_host_template.py index a041c82..792db02 100644 --- a/tests/components/test_component_host_template.py +++ b/tests/components/test_component_host_template.py @@ -38,15 +38,15 @@ async def test_list_host_templates(logger: MagicMock, _list: AsyncMock): results = await list_host_templates(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with(HostTemplate, HostTemplateOrder, filters, limit, page, order) + _list.assert_awaited_once_with(HostTemplate, filters, limit, page, order) # Assert result assert results[0] == host_template -@patch(f"{MODULE}.HostTemplate.create", new_callable=AsyncMock) +@patch(f"{MODULE}._create", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_create_host_template(logger: MagicMock, host_template_create: AsyncMock): +async def test_create_host_template(logger: MagicMock, _create: AsyncMock): # Setup args params = HostTemplateFullParams.model_construct() @@ -54,22 +54,22 @@ async def test_create_host_template(logger: MagicMock, host_template_create: Asy # Mock logger logger.info.return_value = None - # Mock HostTemplate.create - host_template_create.return_value = True + # Mock _create + _create.return_value = True # Call test function result = await create_host_template(params) - # Assert HostTemplate.create called with right args - host_template_create.assert_awaited_once_with(params) + # Assert _create called with right args + _create.assert_awaited_once_with(HostTemplate, params) # Assert result assert result -@patch(f"{MODULE}.HostTemplate.patch", new_callable=AsyncMock) +@patch(f"{MODULE}._patch", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_update_host_template(logger: MagicMock, host_template_patch: AsyncMock): +async def test_update_host_template(logger: MagicMock, _patch: AsyncMock): # Setup args host_id = 10 @@ -78,22 +78,22 @@ async def test_update_host_template(logger: MagicMock, host_template_patch: Asyn # Mock logger logger.info.return_value = None - # Mock HostTemplate.patch - host_template_patch.return_value = True + # Mock _patch + _patch.return_value = True # Call test function result = await update_host_template(host_id, params) - # Assert HostTemplate.patch called with right args - host_template_patch.assert_awaited_once_with(host_id, params) + # Assert _patch called with right args + _patch.assert_awaited_once_with(HostTemplate, host_id, params) # Assert result assert result -@patch(f"{MODULE}.HostTemplate.delete", new_callable=AsyncMock) +@patch(f"{MODULE}._delete", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_delete_host_templates(logger: MagicMock, host_template_delete: AsyncMock): +async def test_delete_host_templates(logger: MagicMock, _delete: AsyncMock): # Setup args host_template_id = 10 @@ -101,14 +101,14 @@ async def test_delete_host_templates(logger: MagicMock, host_template_delete: As # Mock logger logger.info.return_value = None - # Mock HostTemplate.delete - host_template_delete.return_value = True + # Mock _delete + _delete.return_value = {host_template_id: True} # Call test function result = await delete_host_templates([host_template_id]) - # Assert HostTemplate.delete called with right args - host_template_delete.assert_awaited_once_with(host_template_id) + # Assert _delete called with right args + _delete.assert_awaited_once_with(HostTemplate, [host_template_id]) # Assert result assert result == {host_template_id: True} From deb0af1defbe5bac23b4291a7630f498610ca615 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:13:18 +0200 Subject: [PATCH 10/27] Use _list, _create, _delete, _patch for hosts --- centreon_mcp/components/host.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/centreon_mcp/components/host.py b/centreon_mcp/components/host.py index 747e20d..3d2a236 100644 --- a/centreon_mcp/components/host.py +++ b/centreon_mcp/components/host.py @@ -1,10 +1,10 @@ -import asyncio import json from typing import Annotated, Literal from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _create, _delete, _list, _patch from centreon_mcp.types.host import ( Host, HostConfiguration, @@ -13,7 +13,7 @@ HostStatusCount, ) from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder host = FastMCP() @@ -87,7 +87,7 @@ async def list_host_configurations( to avoid retrieving all host configurations except if explicitly intended. """ logger.info("Executing tool list_host_configurations") - return await _list(HostConfiguration, HostConfigurationOrder, filters, limit, page, order) + return await _list(HostConfiguration, filters, limit, page, order) @host.tool( @@ -104,7 +104,7 @@ async def create_host_configuration(params: HostConfigurationFullParams) -> bool Create a host configuration from params. """ logger.info("Executing tool create_host_configuration") - return await HostConfiguration.create(params) + return await _create(HostConfiguration, params) @host.tool( @@ -121,7 +121,7 @@ async def update_host_configuration(host_id: int, params: HostConfigurationParti Update a host configuration from params. """ logger.info("Executing tool update_host_configuration") - return await HostConfiguration.patch(host_id, params) + return await _patch(HostConfiguration, host_id, params) @host.tool( @@ -138,6 +138,4 @@ async def delete_host_configurations(host_ids: list[int]) -> dict[int, bool | Ba Delete multiple host configurations. """ logger.info("Executing tool delete_host_configurations") - tasks = [asyncio.create_task(HostConfiguration.delete(host_id)) for host_id in host_ids] - results = await asyncio.gather(*tasks, return_exceptions=True) - return dict(zip(host_ids, results, strict=True)) + return await _delete(HostConfiguration, host_ids) From cf46004bfdc7dac74c374eca4af7242b0b75bb7c Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:13:24 +0200 Subject: [PATCH 11/27] Use _list, _create, _delete, _patch for hosts --- tests/components/test_component_host.py | 40 ++++++++++++------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/tests/components/test_component_host.py b/tests/components/test_component_host.py index 9dda029..c109fea 100644 --- a/tests/components/test_component_host.py +++ b/tests/components/test_component_host.py @@ -73,17 +73,15 @@ async def test_list_host_configurations(logger: MagicMock, _list: AsyncMock): results = await list_host_configurations(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with( - HostConfiguration, HostConfigurationOrder, filters, limit, page, order - ) + _list.assert_awaited_once_with(HostConfiguration, filters, limit, page, order) # Assert result assert results[0] == host_configuration -@patch(f"{MODULE}.HostConfiguration.create", new_callable=AsyncMock) +@patch(f"{MODULE}._create", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_create_host_configuration(logger: MagicMock, host_configuration_create: AsyncMock): +async def test_create_host_configuration(logger: MagicMock, _create: AsyncMock): # Setup args params = HostConfigurationFullParams.model_construct() @@ -91,22 +89,22 @@ async def test_create_host_configuration(logger: MagicMock, host_configuration_c # Mock logger logger.info.return_value = None - # Mock HostConfiguration.create - host_configuration_create.return_value = True + # Mock _create + _create.return_value = True # Call test function result = await create_host_configuration(params) - # Assert HostConfiguration.create called with right args - host_configuration_create.assert_awaited_once_with(params) + # Assert _create called with right args + _create.assert_awaited_once_with(HostConfiguration, params) # Assert result assert result -@patch(f"{MODULE}.HostConfiguration.patch", new_callable=AsyncMock) +@patch(f"{MODULE}._patch", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_update_host_configuration(logger: MagicMock, host_configuration_patch: AsyncMock): +async def test_update_host_configuration(logger: MagicMock, _patch: AsyncMock): # Setup args host_id = 10 @@ -115,22 +113,22 @@ async def test_update_host_configuration(logger: MagicMock, host_configuration_p # Mock logger logger.info.return_value = None - # Mock HostConfiguration.patch - host_configuration_patch.return_value = True + # Mock _patch + _patch.return_value = True # Call test function result = await update_host_configuration(host_id, params) - # Assert HostConfiguration.patch called with right args - host_configuration_patch.assert_awaited_once_with(host_id, params) + # Assert _patch called with right args + _patch.assert_awaited_once_with(HostConfiguration, host_id, params) # Assert result assert result -@patch(f"{MODULE}.HostConfiguration.delete", new_callable=AsyncMock) +@patch(f"{MODULE}._delete", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_delete_host_configurations(logger: MagicMock, host_configuration_delete: AsyncMock): +async def test_delete_host_configurations(logger: MagicMock, _delete: AsyncMock): # Setup args host_id = 10 @@ -138,14 +136,14 @@ async def test_delete_host_configurations(logger: MagicMock, host_configuration_ # Mock logger logger.info.return_value = None - # Mock HostConfiguration.delete - host_configuration_delete.return_value = True + # Mock _delete + _delete.return_value = {host_id: True} # Call test function result = await delete_host_configurations([host_id]) - # Assert HostConfiguration.delete called with right args - host_configuration_delete.assert_awaited_once_with(host_id) + # Assert _.delete called with right args + _delete.assert_awaited_once_with(HostConfiguration, [host_id]) # Assert result assert result == {host_id: True} From ff8931db68b40e7fd2918802e33a435233d0b974 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:13:42 +0200 Subject: [PATCH 12/27] Use _list for service groups --- centreon_mcp/components/servicegroup.py | 5 +++-- tests/components/test_component_servicegroup.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/centreon_mcp/components/servicegroup.py b/centreon_mcp/components/servicegroup.py index 5417efe..6aa7b4e 100644 --- a/centreon_mcp/components/servicegroup.py +++ b/centreon_mcp/components/servicegroup.py @@ -3,10 +3,11 @@ from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _list from centreon_mcp.types.host import HostState from centreon_mcp.types.servicegroup import ServiceGroup from centreon_mcp.utils import logger -from centreon_mcp.utils.base import BaseFilter, BaseOrder, _list +from centreon_mcp.utils.base import BaseFilter, BaseOrder servicegroup = FastMCP() @@ -59,4 +60,4 @@ async def list_servicegroups( to avoid retrieving all services groups except if explicitly intended. """ logger.info("Executing tool list_servicegroups") - return await _list(ServiceGroup, ServiceGroupOrder, filters, limit, page, order) + return await _list(ServiceGroup, filters, limit, page, order) diff --git a/tests/components/test_component_servicegroup.py b/tests/components/test_component_servicegroup.py index b850ae7..24b5cef 100644 --- a/tests/components/test_component_servicegroup.py +++ b/tests/components/test_component_servicegroup.py @@ -31,7 +31,7 @@ async def test_list_servicegroups(logger: MagicMock, _list: AsyncMock): results = await list_servicegroups(filters, limit, page, order) # Assert _list called with right args - _list.assert_awaited_once_with(ServiceGroup, ServiceGroupOrder, filters, limit, page, order) + _list.assert_awaited_once_with(ServiceGroup, filters, limit, page, order) # Assert result assert results[0] == servicegroup From 65d1d1eb4a1b9a9ef1d20f44098f788f7af595a7 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Tue, 2 Jun 2026 16:58:56 +0200 Subject: [PATCH 13/27] Fix typos --- centreon_mcp/components/base.py | 2 +- tests/components/test_component_host.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/centreon_mcp/components/base.py b/centreon_mcp/components/base.py index 8214205..26fb9d7 100644 --- a/centreon_mcp/components/base.py +++ b/centreon_mcp/components/base.py @@ -16,7 +16,7 @@ async def _list[CentreonModel: ListMixin]( order: BaseOrder | None = None, ) -> list[CentreonModel]: """ - Generic function to list ressources based on provided filters, pagination and order. + Generic function to list resources based on provided filters, pagination and order. """ search = json.dumps(BaseFilter.join(filters)) sort_by = order.model_dump_json() if order else None diff --git a/tests/components/test_component_host.py b/tests/components/test_component_host.py index c109fea..32285d1 100644 --- a/tests/components/test_component_host.py +++ b/tests/components/test_component_host.py @@ -142,7 +142,7 @@ async def test_delete_host_configurations(logger: MagicMock, _delete: AsyncMock) # Call test function result = await delete_host_configurations([host_id]) - # Assert _.delete called with right args + # Assert _delete called with right args _delete.assert_awaited_once_with(HostConfiguration, [host_id]) # Assert result From c22b09c627774237a4374f48cea669598ac00884 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Wed, 3 Jun 2026 11:26:53 +0200 Subject: [PATCH 14/27] Write unit test for _list method --- tests/components/test_component_base.py | 123 ++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/components/test_component_base.py diff --git a/tests/components/test_component_base.py b/tests/components/test_component_base.py new file mode 100644 index 0000000..e5bdc57 --- /dev/null +++ b/tests/components/test_component_base.py @@ -0,0 +1,123 @@ +import json +from unittest.mock import AsyncMock, patch + +import pytest +from pydantic import BaseModel + +from centreon_mcp.components.base import BaseFilter, BaseOrder, _create, _delete, _list +from centreon_mcp.types.acknowledgement import Acknowledgement +from centreon_mcp.types.command import Command +from centreon_mcp.types.downtime import Downtime +from centreon_mcp.types.host import HostConfiguration +from centreon_mcp.types.host_category import ( + HostCategoryConfiguration, + HostCategoryConfigurationFullParams, +) +from centreon_mcp.types.host_group import HostGroupConfiguration, HostGroupConfigurationFullParams +from centreon_mcp.types.host_severity import HostSeverity, HostSeverityFullParams +from centreon_mcp.types.host_template import HostTemplate, HostTemplateFullParams +from centreon_mcp.types.monitoring_server import MonitoringServer +from centreon_mcp.types.servicegroup import ServiceGroup +from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin + +MODULE = "centreon_mcp.components.base" + + +@pytest.mark.parametrize( + "model,params", + [ + (HostCategoryConfiguration, HostCategoryConfigurationFullParams.model_construct()), + (HostGroupConfiguration, HostGroupConfigurationFullParams.model_construct()), + (HostSeverity, HostSeverityFullParams.model_construct()), + (HostTemplate, HostTemplateFullParams.model_construct()), + ], +) +@patch(f"{MODULE}.CreateMixin.create", new_callable=AsyncMock) +async def test_create[CentreonModel: CreateMixin]( + create_mixin: AsyncMock, model: type[CentreonModel], params: BaseModel +): + + # CreateMixin.create + create_mixin.return_value = True + + # Call test function + result = await _create(model, params) + + # Assert CreateMixin.create called with right args + create_mixin.assert_awaited_once_with(params) + + # Assert result + assert result + + +@pytest.mark.parametrize( + "model", + [ + HostConfiguration, + HostGroupConfiguration, + HostCategoryConfiguration, + HostSeverity, + Downtime, + HostTemplate, + ], +) +@patch(f"{MODULE}.DeleteMixin.delete", new_callable=AsyncMock) +async def test_delete[CentreonModel: DeleteMixin]( + delete_mixin: AsyncMock, model: type[CentreonModel] +): + + # Setup args + model_id = 1 + + # Mock DeleteMixin.delete + delete_mixin.return_value = True + + # Call test function + results = await _delete(model, [model_id]) + + # Assert DeleteMixin.delete called with right args + delete_mixin.assert_awaited_once_with(model_id) + + # Assert result + assert results == {model_id: True} + + +@pytest.mark.parametrize( + "model, instance", + [ + (Acknowledgement, Acknowledgement.model_construct()), + (Command, Command.model_construct()), + (HostConfiguration, HostConfiguration.model_construct()), + (HostGroupConfiguration, HostGroupConfiguration.model_construct()), + (HostCategoryConfiguration, HostCategoryConfiguration.model_construct()), + (HostSeverity, HostSeverity.model_construct()), + (Downtime, Downtime.model_construct()), + (HostTemplate, HostTemplate.model_construct()), + (MonitoringServer, MonitoringServer.model_construct()), + (ServiceGroup, ServiceGroup.model_construct()), + ], +) +@patch(f"{MODULE}.ListMixin.list", new_callable=AsyncMock) +async def test_list[CentreonModel: ListMixin]( + list_mixin: AsyncMock, model: type[CentreonModel], instance: CentreonModel +): + + # Setup args + filters = [BaseFilter()] + limit = 10 + page = 1 + order = BaseOrder() + + # Mock ListMixin.list + list_mixin.return_value = [instance] + + # Call test function + results = await _list(model, filters, limit, page, order) + + # Assert ListMixin.list called with right args + search = json.dumps(BaseFilter.join(filters)) + sort_by = order.model_dump_json() + list_mixin.assert_awaited_once_with(search, limit, page, sort_by) + + # Assert result + assert results == [instance] From ffdc40a8b68de608165dcdb7df125898b00103f5 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Wed, 3 Jun 2026 11:33:14 +0200 Subject: [PATCH 15/27] Write unit test for _patch method --- tests/components/test_component_base.py | 39 ++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/tests/components/test_component_base.py b/tests/components/test_component_base.py index e5bdc57..051c19f 100644 --- a/tests/components/test_component_base.py +++ b/tests/components/test_component_base.py @@ -4,21 +4,25 @@ import pytest from pydantic import BaseModel -from centreon_mcp.components.base import BaseFilter, BaseOrder, _create, _delete, _list +from centreon_mcp.components.base import BaseFilter, BaseOrder, _create, _delete, _list, _patch from centreon_mcp.types.acknowledgement import Acknowledgement from centreon_mcp.types.command import Command from centreon_mcp.types.downtime import Downtime -from centreon_mcp.types.host import HostConfiguration +from centreon_mcp.types.host import HostConfiguration, HostConfigurationPartialParams from centreon_mcp.types.host_category import ( HostCategoryConfiguration, HostCategoryConfigurationFullParams, ) from centreon_mcp.types.host_group import HostGroupConfiguration, HostGroupConfigurationFullParams from centreon_mcp.types.host_severity import HostSeverity, HostSeverityFullParams -from centreon_mcp.types.host_template import HostTemplate, HostTemplateFullParams +from centreon_mcp.types.host_template import ( + HostTemplate, + HostTemplateFullParams, + HostTemplatePartialParams, +) from centreon_mcp.types.monitoring_server import MonitoringServer from centreon_mcp.types.servicegroup import ServiceGroup -from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin +from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin, PatchMixin MODULE = "centreon_mcp.components.base" @@ -121,3 +125,30 @@ async def test_list[CentreonModel: ListMixin]( # Assert result assert results == [instance] + + +@pytest.mark.parametrize( + "model,params", + [ + (HostConfiguration, HostConfigurationPartialParams.model_construct()), + (HostTemplate, HostTemplatePartialParams.model_construct()), + ], +) +@patch(f"{MODULE}.PatchMixin.patch", new_callable=AsyncMock) +async def test_patch[CentreonModel: PatchMixin]( + patch_mixin: AsyncMock, model: type[CentreonModel], params: BaseModel +): + # Setup args + model_id = 10 + + # PatchMixin.patch + patch_mixin.return_value = True + + # Call test function + result = await _patch(model, model_id, params) + + # Assert PatchMixin.patch called with right args + patch_mixin.assert_awaited_once_with(model_id, params) + + # Assert result + assert result From b48046ba6a087809bf8d7f38e5dfaac0e42e1816 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Wed, 3 Jun 2026 11:34:56 +0200 Subject: [PATCH 16/27] Remove old _list method --- centreon_mcp/utils/base.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/centreon_mcp/utils/base.py b/centreon_mcp/utils/base.py index 988a2c9..f8ee46f 100644 --- a/centreon_mcp/utils/base.py +++ b/centreon_mcp/utils/base.py @@ -1,11 +1,8 @@ -import json from collections.abc import Sequence from typing import Literal from pydantic import BaseModel -from centreon_mcp.utils.mixins import ListMixin - class BaseOrder(BaseModel): order: Literal["ASC", "DESC"] = "ASC" @@ -32,20 +29,3 @@ def conditions(self) -> list: if value is not None }.items() ] - - -async def _list[CentreonModel: ListMixin, OrderType: BaseOrder, FilterType: BaseFilter]( - model: type[CentreonModel], - order_cls: type[OrderType], - filters: list[FilterType] | None = None, - limit: int = 10, - page: int = 1, - order: OrderType | None = None, -) -> list[CentreonModel]: - """ - Generic function to list ressources in real-time monitoring based on provided filters, pagination and order - """ - order = order or order_cls() - search = json.dumps(BaseFilter.join(filters)) - sort_by = order.model_dump_json() - return await model.list(search, limit, page, sort_by) From a3961eb3f559ccbb220297b6408eee4317567089 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Wed, 3 Jun 2026 14:49:05 +0200 Subject: [PATCH 17/27] Add extras parameter to list mixin to use it for resource --- centreon_mcp/types/resource.py | 54 +++++--------------------- centreon_mcp/utils/mixins.py | 6 ++- tests/types/test_type_resource.py | 63 ------------------------------- tests/utils/test_mixins.py | 19 ++++++++++ 4 files changed, 32 insertions(+), 110 deletions(-) delete mode 100644 tests/types/test_type_resource.py diff --git a/centreon_mcp/types/resource.py b/centreon_mcp/types/resource.py index 889936d..f40e4b7 100644 --- a/centreon_mcp/types/resource.py +++ b/centreon_mcp/types/resource.py @@ -6,7 +6,7 @@ from centreon_mcp.types.base import ResourceType from centreon_mcp.types.host import HostStatus from centreon_mcp.types.service import ServiceStatus -from centreon_mcp.utils.request import request +from centreon_mcp.utils.mixins import ListMixin ResourceStatus = HostStatus | ServiceStatus @@ -17,61 +17,25 @@ class Status(BaseModel): severity_code: int -class Resource(BaseModel): +class Resource(BaseModel, ListMixin): endpoint: ClassVar[str] = "monitoring/resources" uuid: str id: int type: ResourceType name: str - alias: str | None - fqdn: str | None + alias: str | None = None + fqdn: str | None = None host_id: int - service_id: int | None + service_id: int | None = None monitoring_server_name: str is_in_downtime: bool is_acknowledged: bool is_in_flapping: bool status: Status - information: str | None + information: str | None = None has_active_checks_enabled: bool has_passive_checks_enabled: bool - last_status_change: datetime | None - last_check: str | None - tries: str | None - - @classmethod - async def list( - cls, - search: str, - types: str, - statuses: str | None = None, - hostgroup_names: str | None = None, - servicegroup_names: str | None = None, - host_category_names: str | None = None, - service_category_names: str | None = None, - monitoring_server_names: str | None = None, - status_types: str | None = None, - limit: int | None = None, - page: int | None = None, - sort_by: str | None = None, - ) -> list["Resource"]: - """ - List ressources (hosts and services) in real-time monitoring. - """ - params = { - "search": search, - "limit": limit, - "page": page, - "sort_by": sort_by, - "types": types, - "statuses": statuses, - "hostgroup_names": hostgroup_names, - "servicegroup_names": servicegroup_names, - "host_category_names": host_category_names, - "service_category_names": service_category_names, - "monitoring_server_names": monitoring_server_names, - "status_types": status_types, - } - content = await request("GET", cls.endpoint, params=params) - return [cls(**item) for item in content["result"]] + last_status_change: datetime | None = None + last_check: str | None = None + tries: str | None = None diff --git a/centreon_mcp/utils/mixins.py b/centreon_mcp/utils/mixins.py index de8f171..9745733 100644 --- a/centreon_mcp/utils/mixins.py +++ b/centreon_mcp/utils/mixins.py @@ -1,4 +1,4 @@ -from typing import ClassVar, Self +from typing import Any, ClassVar, Self from pydantic import BaseModel @@ -106,10 +106,12 @@ async def list( limit: int | None = None, page: int | None = None, sort_by: str | None = None, + extras: dict[str, Any] | None = None, ) -> list[Self]: """ List resources matching the search string using the model's endpoint. """ - params = {"search": search, "limit": limit, "page": page, "sort_by": sort_by} + extras = extras or {} + params = {"search": search, "limit": limit, "page": page, "sort_by": sort_by, **extras} content = await request("GET", cls.endpoint, params=params) return [cls(**item) for item in content["result"]] diff --git a/tests/types/test_type_resource.py b/tests/types/test_type_resource.py deleted file mode 100644 index fc3aa55..0000000 --- a/tests/types/test_type_resource.py +++ /dev/null @@ -1,63 +0,0 @@ -from unittest.mock import AsyncMock, patch - -from centreon_mcp.types.resource import Resource - -MODULE = "centreon_mcp.types.resource" - - -@patch(f"{MODULE}.request", new_callable=AsyncMock) -async def test_list_resources(request: AsyncMock): - - # Setup args - search = "" - types = "" - statuses = "" - hostgroup_names = "" - servicegroup_names = "" - host_category_names = "" - service_category_names = "" - monitoring_server_names = "" - status_types = "" - limit = 1 - page = 1 - sort_by = "" - - # Mock request - content: dict = {"result": []} - request.return_value = content - - # Call test function - result = await Resource.list( - search, - types, - statuses, - hostgroup_names, - servicegroup_names, - host_category_names, - service_category_names, - monitoring_server_names, - status_types, - limit, - page, - sort_by, - ) - - # Assert request called with right args - params = { - "search": search, - "limit": limit, - "page": page, - "sort_by": sort_by, - "types": types, - "statuses": statuses, - "hostgroup_names": hostgroup_names, - "servicegroup_names": servicegroup_names, - "host_category_names": host_category_names, - "service_category_names": service_category_names, - "monitoring_server_names": monitoring_server_names, - "status_types": status_types, - } - request.assert_awaited_once_with("GET", Resource.endpoint, params=params) - - # Assert result - assert len(result) == 0 diff --git a/tests/utils/test_mixins.py b/tests/utils/test_mixins.py index 268f51c..c92211e 100644 --- a/tests/utils/test_mixins.py +++ b/tests/utils/test_mixins.py @@ -19,6 +19,7 @@ HostTemplatePartialParams, ) from centreon_mcp.types.monitoring_server import MonitoringServer +from centreon_mcp.types.resource import Resource from centreon_mcp.types.servicegroup import ServiceGroup from centreon_mcp.utils.mixins import ( CreateMixin, @@ -327,6 +328,24 @@ async def test_patch_mixin[CentreonModel: PatchMixin]( "is_locked": True, }, ), + ( + Resource, + "monitoring/resources", + { + "id": 10, + "uuid": "resource_uuid", + "type": "host", + "name": "resource_name", + "host_id": 10, + "monitoring_server_name": "poller_name", + "is_in_downtime": False, + "is_acknowledged": False, + "is_in_flapping": False, + "status": {"code": 0, "severity_code": 0, "name": "UP"}, + "has_active_checks_enabled": False, + "has_passive_checks_enabled": False, + }, + ), ], ) @patch(f"{MODULE}.request", new_callable=AsyncMock) From 4175d0e3ec7bf8f5b71c4f7817e9dfaf3a137c81 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Wed, 3 Jun 2026 14:49:47 +0200 Subject: [PATCH 18/27] Use generic _list method for resources --- centreon_mcp/components/base.py | 4 +- centreon_mcp/components/resource.py | 28 +++++---- tests/components/test_component_base.py | 7 ++- tests/components/test_component_resource.py | 65 ++++++--------------- 4 files changed, 40 insertions(+), 64 deletions(-) diff --git a/centreon_mcp/components/base.py b/centreon_mcp/components/base.py index 26fb9d7..677e315 100644 --- a/centreon_mcp/components/base.py +++ b/centreon_mcp/components/base.py @@ -1,6 +1,7 @@ import asyncio import json from collections.abc import Sequence +from typing import Any from pydantic import BaseModel @@ -14,13 +15,14 @@ async def _list[CentreonModel: ListMixin]( limit: int = 10, page: int = 1, order: BaseOrder | None = None, + extras: dict[str, Any] | None = None, ) -> list[CentreonModel]: """ Generic function to list resources based on provided filters, pagination and order. """ search = json.dumps(BaseFilter.join(filters)) sort_by = order.model_dump_json() if order else None - return await model.list(search, limit, page, sort_by) + return await model.list(search, limit, page, sort_by, extras) async def _delete[CentreonModel: DeleteMixin]( diff --git a/centreon_mcp/components/resource.py b/centreon_mcp/components/resource.py index b5b0e54..cb12be8 100644 --- a/centreon_mcp/components/resource.py +++ b/centreon_mcp/components/resource.py @@ -4,6 +4,7 @@ from fastmcp import FastMCP from pydantic import Field +from centreon_mcp.components.base import _list from centreon_mcp.types.base import ResourceType, StatusType from centreon_mcp.types.resource import Resource, ResourceStatus from centreon_mcp.utils import logger @@ -74,18 +75,15 @@ async def list_resources( to avoid retrieving all resources except if explicitly intended. """ logger.info("Executing tool list_resources") - order = order or ResourceOrder() - return await Resource.list( - search=json.dumps(ResourceFilter.join(filters)), - types=json.dumps(types or []), - statuses=json.dumps(statuses or []), - hostgroup_names=json.dumps(hostgroup_names or []), - servicegroup_names=json.dumps(servicegroup_names or []), - host_category_names=json.dumps(host_category_names or []), - service_category_names=json.dumps(service_category_names or []), - monitoring_server_names=json.dumps(monitoring_server_names or []), - status_types=json.dumps(status_types or []), - limit=limit, - page=page, - sort_by=order.model_dump_json(), - ) + fields = { + "types": types, + "statuses": statuses, + "hostgroup_names": hostgroup_names, + "servicegroup_names": servicegroup_names, + "host_category_names": host_category_names, + "service_category_names": service_category_names, + "monitoring_server_names": monitoring_server_names, + "status_types": status_types, + } + extras = {name: json.dumps(value) for name, value in fields.items() if value} + return await _list(Resource, filters, limit, page, order, extras) diff --git a/tests/components/test_component_base.py b/tests/components/test_component_base.py index 051c19f..56018c3 100644 --- a/tests/components/test_component_base.py +++ b/tests/components/test_component_base.py @@ -21,6 +21,7 @@ HostTemplatePartialParams, ) from centreon_mcp.types.monitoring_server import MonitoringServer +from centreon_mcp.types.resource import Resource from centreon_mcp.types.servicegroup import ServiceGroup from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin, PatchMixin @@ -99,6 +100,7 @@ async def test_delete[CentreonModel: DeleteMixin]( (HostTemplate, HostTemplate.model_construct()), (MonitoringServer, MonitoringServer.model_construct()), (ServiceGroup, ServiceGroup.model_construct()), + (Resource, Resource.model_construct()), ], ) @patch(f"{MODULE}.ListMixin.list", new_callable=AsyncMock) @@ -111,17 +113,18 @@ async def test_list[CentreonModel: ListMixin]( limit = 10 page = 1 order = BaseOrder() + extras = None # Mock ListMixin.list list_mixin.return_value = [instance] # Call test function - results = await _list(model, filters, limit, page, order) + results = await _list(model, filters, limit, page, order, extras) # Assert ListMixin.list called with right args search = json.dumps(BaseFilter.join(filters)) sort_by = order.model_dump_json() - list_mixin.assert_awaited_once_with(search, limit, page, sort_by) + list_mixin.assert_awaited_once_with(search, limit, page, sort_by, extras) # Assert result assert results == [instance] diff --git a/tests/components/test_component_resource.py b/tests/components/test_component_resource.py index b400bea..bfae744 100644 --- a/tests/components/test_component_resource.py +++ b/tests/components/test_component_resource.py @@ -2,74 +2,47 @@ from unittest.mock import AsyncMock, MagicMock, patch from centreon_mcp.components.resource import ResourceFilter, ResourceOrder, list_resources -from centreon_mcp.types.base import ResourceType, StatusType -from centreon_mcp.types.resource import Resource, ResourceStatus +from centreon_mcp.types.resource import Resource MODULE = "centreon_mcp.components.resource" -@patch(f"{MODULE}.Resource.list", new_callable=AsyncMock) -@patch(f"{MODULE}.ResourceFilter.join", new_callable=MagicMock) +@patch(f"{MODULE}._list", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_list_resources(logger: MagicMock, join: MagicMock, resource_list: AsyncMock): +async def test_list_resources(logger: MagicMock, _list: AsyncMock): # Setup args filters = [ResourceFilter.model_construct()] - types: list[ResourceType] = ["host", "service"] - statuses: list[ResourceStatus] = ["UP", "WARNING"] - hostgroup_names = ["hostgroup_name"] - servicegroup_names = ["servicegroup_name"] - host_category_names = ["host_category_name"] - service_category_names = ["service_category_name"] - monitoring_server_names = ["monitoring_server_name"] - status_types: list[StatusType] = ["hard"] limit = 50 page = 1 order = ResourceOrder() + hostgroup_names = ["hostgroup_name_10"] + monitoring_server_names = ["monitoring_server_name_10"] # Mock logger - logger.debug.return_value = None + logger.info.return_value = None - # Mock ResourceFilter.join - conditions: dict = {} - join.return_value = conditions - - # Mock request + # Mock _list resource = Resource.model_construct() - resource_list.return_value = [resource] + _list.return_value = [resource] # Call test function results = await list_resources( filters, - types, - statuses, - hostgroup_names, - servicegroup_names, - host_category_names, - service_category_names, - monitoring_server_names, - status_types, - limit, - page, - order, - ) - - # Assert request called with right args - sort_by = order.model_dump_json() - resource_list.assert_awaited_once_with( - search=json.dumps(conditions), - types=json.dumps(types or []), - statuses=json.dumps(statuses or []), - hostgroup_names=json.dumps(hostgroup_names or []), - servicegroup_names=json.dumps(servicegroup_names or []), - host_category_names=json.dumps(host_category_names or []), - service_category_names=json.dumps(service_category_names or []), - monitoring_server_names=json.dumps(monitoring_server_names or []), - status_types=json.dumps(status_types or []), limit=limit, page=page, - sort_by=sort_by, + order=order, + hostgroup_names=hostgroup_names, + monitoring_server_names=monitoring_server_names, ) + # Assert _list called with right args + fields = { + "hostgroup_names": hostgroup_names, + "monitoring_server_names": monitoring_server_names, + } + extras = {name: json.dumps(value) for name, value in fields.items() if value} + _list.assert_awaited_once_with(Resource, filters, limit, page, order, extras) + # Assert result assert results[0] == resource From 5143c52a0c76cea2a57f09783878a836141977fc Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Wed, 3 Jun 2026 16:03:08 +0200 Subject: [PATCH 19/27] Update host group configuration model to simplify update tool --- centreon_mcp/components/host_group.py | 7 ++----- centreon_mcp/types/host_group.py | 10 +++------- tests/components/test_component_host_group.py | 14 ++------------ 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/centreon_mcp/components/host_group.py b/centreon_mcp/components/host_group.py index 9eea6a4..041f5d1 100644 --- a/centreon_mcp/components/host_group.py +++ b/centreon_mcp/components/host_group.py @@ -121,14 +121,11 @@ async def update_host_group_configuration( host_group_id: int, params: HostGroupConfigurationPartialParams ) -> bool: """ - Update a host group from params. + Update a host group from params. Just need to get host_group_id first. """ logger.info("Executing tool update_host_group_configuration") hostgroup = await HostGroupConfiguration.get(host_group_id) - data = hostgroup.model_dump(exclude={"id", "is_activated", "icon", "hosts"}, exclude_none=True) - data["icon_id"] = hostgroup.icon.id if hostgroup.icon else None - data["hosts"] = [host.id for host in hostgroup.hosts if host.id not in params.hosts_removed] - data["hosts"] += [host_id for host_id in params.hosts_added if host_id not in data["hosts"]] + data = hostgroup.model_dump(exclude={"id"}, exclude_none=True) data |= params.model_dump(exclude_none=True) return await HostGroupConfiguration.update( host_group_id, HostGroupConfigurationFullParams(**data) diff --git a/centreon_mcp/types/host_group.py b/centreon_mcp/types/host_group.py index acf0f14..64a6480 100644 --- a/centreon_mcp/types/host_group.py +++ b/centreon_mcp/types/host_group.py @@ -1,6 +1,6 @@ from typing import ClassVar -from pydantic import BaseModel, Field +from pydantic import AliasPath, BaseModel, Field from centreon_mcp.types.base import Link from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin, ReadMixin, UpdateMixin @@ -12,8 +12,6 @@ "geo_coords": "Geographical coordinates use by Centreon Map module to position element on map", "comment": "Comments on this host group", "hosts": "Hosts linked to this host group", - "hosts_added": "Ids of the hosts to add to the host group.", - "hosts_removed": "Ids of the hosts to remove from the host group.", } @@ -35,17 +33,15 @@ class HostGroupConfigurationBaseParams(BaseModel): icon_id: int | None = Field(None, description=DESCRIPTION["icon_id"]) geo_coords: str | None = Field(None, description=DESCRIPTION["geo_coords"]) comment: str | None = Field(None, description=DESCRIPTION["comment"]) + hosts: list[int] | None = Field(None, description=DESCRIPTION["hosts"]) class HostGroupConfigurationPartialParams(HostGroupConfigurationBaseParams): name: str | None = Field(None, description=DESCRIPTION["name"]) - hosts_added: list[int] = Field(default_factory=list, description=DESCRIPTION["hosts_added"]) - hosts_removed: list[int] = Field(default_factory=list, description=DESCRIPTION["hosts_removed"]) class HostGroupConfigurationFullParams(HostGroupConfigurationBaseParams): name: str = Field(description=DESCRIPTION["name"]) - hosts: list[int] | None = Field(None, description=DESCRIPTION["hosts"]) class HostGroupConfiguration( @@ -61,7 +57,7 @@ class HostGroupConfiguration( id: int name: str alias: str | None = None - icon: Icon | None = None + icon_id: int | None = Field(None, validation_alias=AliasPath("icon", "id")) geo_coords: str | None = None comment: str | None = None is_activated: bool diff --git a/tests/components/test_component_host_group.py b/tests/components/test_component_host_group.py index 81ffe0f..4488e50 100644 --- a/tests/components/test_component_host_group.py +++ b/tests/components/test_component_host_group.py @@ -11,13 +11,11 @@ list_host_groups, update_host_group_configuration, ) -from centreon_mcp.types.base import Link from centreon_mcp.types.host_group import ( HostGroup, HostGroupConfiguration, HostGroupConfigurationFullParams, HostGroupConfigurationPartialParams, - Icon, ) MODULE = "centreon_mcp.components.host_group" @@ -117,12 +115,7 @@ async def test_update_host_group_configuration( logger.info.return_value = None # Mock HostGroupConfiguration.get - hostgroup = HostGroupConfiguration.model_construct( - id=1, - name="HostGroup", - icon=Icon.model_construct(id=1), - hosts=[Link(id=10, name="host_name_10")], - ) + hostgroup = HostGroupConfiguration.model_construct(id=1, name="HostGroup") hostgroup_configuration_get.return_value = hostgroup # Mock HostGroupConfiguration.update @@ -135,10 +128,7 @@ async def test_update_host_group_configuration( hostgroup_configuration_get.assert_awaited_once_with(hostgroup_id) # Assert HostSeverity.update called with right args - data = hostgroup.model_dump(exclude={"id", "is_activated", "icon", "hosts"}) - data["icon_id"] = hostgroup.icon.id if hostgroup.icon else None - data["hosts"] = [host.id for host in hostgroup.hosts if host.id not in params.hosts_removed] - data["hosts"] += [host_id for host_id in params.hosts_added if host_id not in data["hosts"]] + data = hostgroup.model_dump(exclude={"id", "is_activated"}) data |= params.model_dump(exclude_none=True) hostgroup_configuration_update.assert_awaited_once_with( hostgroup_id, HostGroupConfigurationFullParams(**data) From a4c70049d949364f5bbb55b682f9708122712272 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Thu, 4 Jun 2026 15:28:49 +0200 Subject: [PATCH 20/27] Inherit update mixin from read one for typing issue --- centreon_mcp/utils/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centreon_mcp/utils/mixins.py b/centreon_mcp/utils/mixins.py index 9745733..a6717bd 100644 --- a/centreon_mcp/utils/mixins.py +++ b/centreon_mcp/utils/mixins.py @@ -56,7 +56,7 @@ async def delete(cls, model_id: int) -> bool: return True -class UpdateMixin[Params: BaseModel]: +class UpdateMixin[Params: BaseModel](ReadMixin): """ Mixin to add to a Centreon Model a update method via heritage """ From 7c5c2094dd0c0560e9c9f3554d9a60bc9d488f91 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Thu, 4 Jun 2026 15:29:37 +0200 Subject: [PATCH 21/27] Define generic method to update resource --- centreon_mcp/components/base.py | 20 ++++++- tests/components/test_component_base.py | 77 +++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/centreon_mcp/components/base.py b/centreon_mcp/components/base.py index 677e315..1f95ad2 100644 --- a/centreon_mcp/components/base.py +++ b/centreon_mcp/components/base.py @@ -6,7 +6,13 @@ from pydantic import BaseModel from centreon_mcp.utils.base import BaseFilter, BaseOrder -from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin, PatchMixin +from centreon_mcp.utils.mixins import ( + CreateMixin, + DeleteMixin, + ListMixin, + PatchMixin, + UpdateMixin, +) async def _list[CentreonModel: ListMixin]( @@ -52,3 +58,15 @@ async def _patch[CentreonModel: PatchMixin]( Generic function to patch a resource based on params. """ return await model.patch(model_id, params) + + +async def _update[CentreonModel: UpdateMixin, FullParams: BaseModel]( + model: type[CentreonModel], full_params_cls: type[FullParams], model_id: int, params: BaseModel +) -> bool: + """ + Generic function to update a resource from params. + """ + current = await model.get(model_id) + data = current.model_dump(exclude={"id"}, exclude_none=True) # type: ignore + data |= params.model_dump(exclude_none=True) + return await model.update(model_id, full_params_cls(**data)) diff --git a/tests/components/test_component_base.py b/tests/components/test_component_base.py index 56018c3..58e0f07 100644 --- a/tests/components/test_component_base.py +++ b/tests/components/test_component_base.py @@ -4,7 +4,15 @@ import pytest from pydantic import BaseModel -from centreon_mcp.components.base import BaseFilter, BaseOrder, _create, _delete, _list, _patch +from centreon_mcp.components.base import ( + BaseFilter, + BaseOrder, + _create, + _delete, + _list, + _patch, + _update, +) from centreon_mcp.types.acknowledgement import Acknowledgement from centreon_mcp.types.command import Command from centreon_mcp.types.downtime import Downtime @@ -12,9 +20,14 @@ from centreon_mcp.types.host_category import ( HostCategoryConfiguration, HostCategoryConfigurationFullParams, + HostCategoryConfigurationPartialParams, ) from centreon_mcp.types.host_group import HostGroupConfiguration, HostGroupConfigurationFullParams -from centreon_mcp.types.host_severity import HostSeverity, HostSeverityFullParams +from centreon_mcp.types.host_severity import ( + HostSeverity, + HostSeverityFullParams, + HostSeverityPartialParams, +) from centreon_mcp.types.host_template import ( HostTemplate, HostTemplateFullParams, @@ -23,7 +36,7 @@ from centreon_mcp.types.monitoring_server import MonitoringServer from centreon_mcp.types.resource import Resource from centreon_mcp.types.servicegroup import ServiceGroup -from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin, PatchMixin +from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin, PatchMixin, UpdateMixin MODULE = "centreon_mcp.components.base" @@ -144,7 +157,7 @@ async def test_patch[CentreonModel: PatchMixin]( # Setup args model_id = 10 - # PatchMixin.patch + # Mock PatchMixin.patch patch_mixin.return_value = True # Call test function @@ -155,3 +168,59 @@ async def test_patch[CentreonModel: PatchMixin]( # Assert result assert result + + +@pytest.mark.parametrize( + "model_cls,full_params_cls,partial_params", + [ + ( + HostGroupConfiguration, + HostGroupConfigurationFullParams, + HostConfigurationPartialParams.model_construct(name="host_group_name"), + ), + ( + HostCategoryConfiguration, + HostCategoryConfigurationFullParams, + HostCategoryConfigurationPartialParams.model_construct( + name="host_category_name", alias="host_category_alias" + ), + ), + ( + HostSeverity, + HostGroupConfigurationFullParams, + HostSeverityPartialParams.model_construct(name="host_severity_name"), + ), + ], +) +@patch(f"{MODULE}.UpdateMixin.update", new_callable=AsyncMock) +@patch(f"{MODULE}.UpdateMixin.get", new_callable=AsyncMock) +async def test_update[CentreonModel: UpdateMixin]( + get_mixin: AsyncMock, + update_mixin: AsyncMock, + model_cls: type[CentreonModel], + full_params_cls: type[BaseModel], + partial_params: BaseModel, +): + # Setup args + model_id = 10 + + # Mock ReadMixin.get + model = model_cls.model_construct() # type: ignore + get_mixin.return_value = model + + # Mock UpdateMixin.update + update_mixin.return_value = True + + # Call test function + result = await _update(model_cls, full_params_cls, model_id, partial_params) + + # Assert ReadMixin.get called with right args + get_mixin.assert_awaited_once_with(model_id) + + # Assert UpdateMixin.update called with right args + data = model.model_dump(exclude={"id"}, exclude_none=True) + data |= partial_params.model_dump(exclude_none=True) + update_mixin.assert_awaited_once_with(model_id, full_params_cls(**data)) + + # Assert result + assert result From 2f767f7d718370f211bc3a98d8d13de755c1dbeb Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Thu, 4 Jun 2026 15:30:21 +0200 Subject: [PATCH 22/27] Use generic _update for host categories --- centreon_mcp/components/host_category.py | 9 ++---- .../test_component_host_category.py | 29 +++++-------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/centreon_mcp/components/host_category.py b/centreon_mcp/components/host_category.py index cfdc23b..29c2ce0 100644 --- a/centreon_mcp/components/host_category.py +++ b/centreon_mcp/components/host_category.py @@ -3,7 +3,7 @@ from fastmcp import FastMCP from pydantic import Field -from centreon_mcp.components.base import _create, _delete, _list +from centreon_mcp.components.base import _create, _delete, _list, _update from centreon_mcp.types.host_category import ( HostCategoryConfiguration, HostCategoryConfigurationFullParams, @@ -83,11 +83,8 @@ async def update_host_category_configuration( Update a host category from params. """ logger.info("Executing tool update_host_category_configuration") - host_category = await HostCategoryConfiguration.get(host_category_id) - data = host_category.model_dump(exclude={"id"}, exclude_none=True) - data |= params.model_dump(exclude_none=True) - return await HostCategoryConfiguration.update( - host_category_id, HostCategoryConfigurationFullParams(**data) + return await _update( + HostCategoryConfiguration, HostCategoryConfigurationFullParams, host_category_id, params ) diff --git a/tests/components/test_component_host_category.py b/tests/components/test_component_host_category.py index 90b3230..92327d8 100644 --- a/tests/components/test_component_host_category.py +++ b/tests/components/test_component_host_category.py @@ -67,14 +67,9 @@ async def test_create_host_category_configuration(logger: MagicMock, _create: As assert result -@patch(f"{MODULE}.HostCategoryConfiguration.update", new_callable=AsyncMock) -@patch(f"{MODULE}.HostCategoryConfiguration.get", new_callable=AsyncMock) +@patch(f"{MODULE}._update", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_update_host_category_configuration( - logger: MagicMock, - host_category_configuration_get: AsyncMock, - host_category_configuration_update: AsyncMock, -): +async def test_update_host_category(logger: MagicMock, _update: AsyncMock): # Setup args host_category_id = 10 @@ -83,25 +78,15 @@ async def test_update_host_category_configuration( # Mock logger logger.info.return_value = None - # Mock HostCategoryConfiguration.get - host_category = HostCategoryConfiguration.model_construct( - name="host_category_name", alias="host_category_alias" - ) - host_category_configuration_get.return_value = host_category - - # Mock HostCategoryConfiguration.update - host_category_configuration_update.return_value = True + # Mock _update + _update.return_value = True # Call test function result = await update_host_category_configuration(host_category_id, params) - # Assert HostCategoryConfiguration.get called with right args - host_category_configuration_get.assert_awaited_once_with(host_category_id) - - # Assert HostCategory.update called with right args - data = host_category.model_dump(exclude={"id"}) | params.model_dump(exclude_none=True) - host_category_configuration_update.assert_awaited_once_with( - host_category_id, HostCategoryConfigurationFullParams(**data) + # Assert _update called with right args + _update.assert_awaited_once_with( + HostCategoryConfiguration, HostCategoryConfigurationFullParams, host_category_id, params ) # Assert result From d2f3a0535efb0c798c0416294d8860c26968c2a3 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Thu, 4 Jun 2026 15:30:41 +0200 Subject: [PATCH 23/27] Use generic _update for host severities --- centreon_mcp/components/host_severity.py | 6 ++--- .../test_component_host_severity.py | 23 +++++-------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/centreon_mcp/components/host_severity.py b/centreon_mcp/components/host_severity.py index 6131928..8ab962a 100644 --- a/centreon_mcp/components/host_severity.py +++ b/centreon_mcp/components/host_severity.py @@ -3,7 +3,7 @@ from fastmcp import FastMCP from pydantic import Field -from centreon_mcp.components.base import _create, _delete, _list +from centreon_mcp.components.base import _create, _delete, _list, _update from centreon_mcp.types.host_severity import ( HostSeverity, HostSeverityFullParams, @@ -83,9 +83,7 @@ async def update_host_severity(host_severity_id: int, params: HostSeverityPartia Update a host severity from params. """ logger.info("Executing tool update_host_severity") - host_severity = await HostSeverity.get(host_severity_id) - data = host_severity.model_dump(exclude={"id"}) | params.model_dump(exclude_none=True) - return await HostSeverity.update(host_severity_id, HostSeverityFullParams(**data)) + return await _update(HostSeverity, HostSeverityFullParams, host_severity_id, params) @host_severity.tool( diff --git a/tests/components/test_component_host_severity.py b/tests/components/test_component_host_severity.py index 9dc2913..431cf66 100644 --- a/tests/components/test_component_host_severity.py +++ b/tests/components/test_component_host_severity.py @@ -67,12 +67,9 @@ async def test_create_host_severity(logger: MagicMock, _create: AsyncMock): assert result -@patch(f"{MODULE}.HostSeverity.update", new_callable=AsyncMock) -@patch(f"{MODULE}.HostSeverity.get", new_callable=AsyncMock) +@patch(f"{MODULE}._update", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_update_host_severity( - logger: MagicMock, host_severity_get: AsyncMock, host_severity_update: AsyncMock -): +async def test_update_host_severity(logger: MagicMock, _update: AsyncMock): # Setup args host_severity_id = 10 @@ -81,22 +78,14 @@ async def test_update_host_severity( # Mock logger logger.info.return_value = None - # Mock HostSeverity.get - host_severity = HostSeverity.model_construct(name="Nmae", alias="Alias", level=1, icon_id=1) - host_severity_get.return_value = host_severity - - # Mock HostSeverity.update - host_severity_update.return_value = True + # Mock _update + _update.return_value = True # Call test function result = await update_host_severity(host_severity_id, params) - # Assert HostSeverity.get called with right args - host_severity_get.assert_awaited_once_with(host_severity_id) - - # Assert HostSeverity.update called with right args - data = host_severity.model_dump(exclude={"id"}) | params.model_dump(exclude_none=True) - host_severity_update.assert_awaited_once_with(host_severity_id, HostSeverityFullParams(**data)) + # Assert _update called with right args + _update.assert_awaited_once_with(HostSeverity, HostSeverityFullParams, host_severity_id, params) # Assert result assert result From dc92e78022dd4cc76ce4a22484a628e6e8fa68ef Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Thu, 4 Jun 2026 15:30:58 +0200 Subject: [PATCH 24/27] Use generic _update for host groups --- centreon_mcp/components/host_group.py | 9 ++---- tests/components/test_component_host_group.py | 32 ++++++------------- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/centreon_mcp/components/host_group.py b/centreon_mcp/components/host_group.py index 041f5d1..6a220c7 100644 --- a/centreon_mcp/components/host_group.py +++ b/centreon_mcp/components/host_group.py @@ -3,7 +3,7 @@ from fastmcp import FastMCP from pydantic import Field -from centreon_mcp.components.base import _create, _delete, _list +from centreon_mcp.components.base import _create, _delete, _list, _update from centreon_mcp.types.host import HostState from centreon_mcp.types.host_group import ( HostGroup, @@ -124,11 +124,8 @@ async def update_host_group_configuration( Update a host group from params. Just need to get host_group_id first. """ logger.info("Executing tool update_host_group_configuration") - hostgroup = await HostGroupConfiguration.get(host_group_id) - data = hostgroup.model_dump(exclude={"id"}, exclude_none=True) - data |= params.model_dump(exclude_none=True) - return await HostGroupConfiguration.update( - host_group_id, HostGroupConfigurationFullParams(**data) + return await _update( + HostGroupConfiguration, HostGroupConfigurationFullParams, host_group_id, params ) diff --git a/tests/components/test_component_host_group.py b/tests/components/test_component_host_group.py index 4488e50..0c333ff 100644 --- a/tests/components/test_component_host_group.py +++ b/tests/components/test_component_host_group.py @@ -98,40 +98,26 @@ async def test_create_host_group_configuration(logger: MagicMock, _create: Async assert result -@patch(f"{MODULE}.HostGroupConfiguration.update", new_callable=AsyncMock) -@patch(f"{MODULE}.HostGroupConfiguration.get", new_callable=AsyncMock) +@patch(f"{MODULE}._update", new_callable=AsyncMock) @patch(f"{MODULE}.logger", new_callable=MagicMock) -async def test_update_host_group_configuration( - logger: MagicMock, - hostgroup_configuration_get: AsyncMock, - hostgroup_configuration_update: AsyncMock, -): +async def test_update_host_severity(logger: MagicMock, _update: AsyncMock): # Setup args - hostgroup_id = 10 + host_group_id = 10 params = HostGroupConfigurationPartialParams.model_construct() # Mock logger logger.info.return_value = None - # Mock HostGroupConfiguration.get - hostgroup = HostGroupConfiguration.model_construct(id=1, name="HostGroup") - hostgroup_configuration_get.return_value = hostgroup - - # Mock HostGroupConfiguration.update - hostgroup_configuration_update.return_value = True + # Mock _update + _update.return_value = True # Call test function - result = await update_host_group_configuration(hostgroup_id, params) - - # Assert HostGrougConfiguration.get called with right args - hostgroup_configuration_get.assert_awaited_once_with(hostgroup_id) + result = await update_host_group_configuration(host_group_id, params) - # Assert HostSeverity.update called with right args - data = hostgroup.model_dump(exclude={"id", "is_activated"}) - data |= params.model_dump(exclude_none=True) - hostgroup_configuration_update.assert_awaited_once_with( - hostgroup_id, HostGroupConfigurationFullParams(**data) + # Assert _update called with right args + _update.assert_awaited_once_with( + HostGroupConfiguration, HostGroupConfigurationFullParams, host_group_id, params ) # Assert result From 1109836c930cebdab4f8066c5a6157a742cba8c6 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Thu, 4 Jun 2026 16:13:38 +0200 Subject: [PATCH 25/27] No need icon class anymore --- centreon_mcp/types/host_group.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/centreon_mcp/types/host_group.py b/centreon_mcp/types/host_group.py index 64a6480..5e95030 100644 --- a/centreon_mcp/types/host_group.py +++ b/centreon_mcp/types/host_group.py @@ -15,12 +15,6 @@ } -class Icon(BaseModel): - id: int - name: str - url: str - - class HostGroup(BaseModel, ListMixin): endpoint: ClassVar[str] = "monitoring/hostgroups" From bfc8282c46d50b1aa43ebc5e36b3d51af98475c5 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Thu, 4 Jun 2026 16:35:40 +0200 Subject: [PATCH 26/27] Keep only hosts ids to be aligned with base params --- centreon_mcp/types/host_group.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/centreon_mcp/types/host_group.py b/centreon_mcp/types/host_group.py index 5e95030..03e8ef5 100644 --- a/centreon_mcp/types/host_group.py +++ b/centreon_mcp/types/host_group.py @@ -1,8 +1,7 @@ from typing import ClassVar -from pydantic import AliasPath, BaseModel, Field +from pydantic import AliasPath, BaseModel, Field, field_validator -from centreon_mcp.types.base import Link from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin, ReadMixin, UpdateMixin DESCRIPTION = { @@ -57,4 +56,12 @@ class HostGroupConfiguration( is_activated: bool enabled_hosts_count: int | None = None disabled_hosts_count: int | None = None - hosts: list[Link] = Field(default_factory=list) + hosts: list[int] = Field(default_factory=list) + + @field_validator("hosts", mode="before") + @classmethod + def validate_hosts(cls, hosts: list[dict]) -> list[int]: + """ + Convert list of Link to list of int to be aligned with params. + """ + return [host["id"] for host in hosts] From 1e459403344a8c76afa8a4dd321058b36e7f9695 Mon Sep 17 00:00:00 2001 From: gregory-lvtx Date: Thu, 4 Jun 2026 16:44:58 +0200 Subject: [PATCH 27/27] Test _delete with one succes and one error --- tests/components/test_component_base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/components/test_component_base.py b/tests/components/test_component_base.py index 58e0f07..d75a1f2 100644 --- a/tests/components/test_component_base.py +++ b/tests/components/test_component_base.py @@ -1,5 +1,5 @@ import json -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, call, patch import pytest from pydantic import BaseModel @@ -37,6 +37,7 @@ from centreon_mcp.types.resource import Resource from centreon_mcp.types.servicegroup import ServiceGroup from centreon_mcp.utils.mixins import CreateMixin, DeleteMixin, ListMixin, PatchMixin, UpdateMixin +from centreon_mcp.utils.request import CentreonAPIError MODULE = "centreon_mcp.components.base" @@ -85,19 +86,20 @@ async def test_delete[CentreonModel: DeleteMixin]( ): # Setup args - model_id = 1 + model_ids = [1, 2] # Mock DeleteMixin.delete - delete_mixin.return_value = True + error = CentreonAPIError(404, "fake_url", "GET", {}) + delete_mixin.side_effect = [True, error] # Call test function - results = await _delete(model, [model_id]) + results = await _delete(model, model_ids) # Assert DeleteMixin.delete called with right args - delete_mixin.assert_awaited_once_with(model_id) + delete_mixin.assert_has_awaits([call(model_id) for model_id in model_ids]) # Assert result - assert results == {model_id: True} + assert results == {model_ids[0]: True, model_ids[1]: error} @pytest.mark.parametrize(