Add Smart Endpoints framework for ND API v1#186
Add Smart Endpoints framework for ND API v1#186allenrobel wants to merge 21 commits intond42_integrationfrom
Conversation
Base classes: - plugins/module_utils/ep/base_path.py: ApiPath base class for type-safe endpoint path construction - plugins/module_utils/ep/base_paths_ndfc.py: NDFC base path constants - plugins/module_utils/ep/endpoint_mixins.py: Reusable endpoint mixins (e.g. LoginIdMixin) - plugins/module_utils/ep/query_params.py: EndpointQueryParams, LuceneQueryParams, CompositeQueryParams for structured query string building ND API v1 endpoints: - plugins/module_utils/ep/v1/base_paths_infra.py: Infra API base paths - plugins/module_utils/ep/v1/base_paths_manage.py: Manage API base paths - plugins/module_utils/ep/v1/ep_infra_aaa.py: AAA (login) endpoint - plugins/module_utils/ep/v1/ep_infra_clusterhealth.py: Cluster health endpoint - plugins/module_utils/ep/v1/ep_manage_switches.py: Switch management endpoint Design documentation: - plugins/module_utils/ep/ANALYSIS_COMPLEXITY.md - plugins/module_utils/ep/ANALYSIS_ENDPOINT_DESIGN.md - plugins/module_utils/ep/ANALYSIS_TYPE_SAFETY.md Unit tests: - tests/unit/module_utils/ep/test_base_path.py - tests/unit/module_utils/ep/test_base_paths_infra.py - tests/unit/module_utils/ep/test_base_paths_manage.py - tests/unit/module_utils/ep/test_endpoint_mixins.py - tests/unit/module_utils/ep/test_ep_api_v1_infra_aaa.py - tests/unit/module_utils/ep/test_ep_api_v1_infra_clusterhealth.py - tests/unit/module_utils/ep/test_ep_api_v1_manage_switches.py - tests/unit/module_utils/ep/test_query_params.py Note: tests depend on enums.py, pydantic_compat.py, and shared test infrastructure (common_utils, etc.) from nd42_rest_send, and on log.py from nd42_logging. Tests will not run until all branches are merged into nd42_integration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Remove /mso, /appcenter/*, /login paths TODO: Need to add /api/v1/infra/login under infra endpoints.
Removed the following from all files: # -*- coding: utf-8 -*-
- plugins/module_utils/ep/ → plugins/module_utils/endpoints/ - v1/ep_infra_aaa.py → v1/infra_aaa.py - v1/ep_infra_clusterhealth.py → v1/infra_clusterhealth.py - v1/ep_manage_switches.py → v1/manage_switches.py - tests/unit/module_utils/ep/ → tests/unit/module_utils/endpoints/ - test_ep_api_v1_*.py → test_endpoints_api_v1_*.py - Update all internal imports to reflect new paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the test_ep_ prefix in all test function names within the three ep-specific test files to match the renamed file and directory. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ansible sanity tests use a regex to verify the __metaclass__ = type declaration and do not expect an inline comment. Move the pylint disable/enable directives to separate lines. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| @@ -0,0 +1,89 @@ | |||
| # Copyright: (c) 2026, Allen Robel (@arobel) <arobel@cisco.com> | |||
There was a problem hiding this comment.
do we need endpoint_ in this name can be mixins.py
There was a problem hiding this comment.
Akshay has mixins and others will have mixins so should we put them all in onen file called mixins.py or only put "common" things in mixins.py and use a module_name directory or some other way to separate mixin files?
There was a problem hiding this comment.
Renamed to mixins.py with commit: 26cbd52
Endpoint mixins are associated with endpoints rather than than modules. So, I'd lean toward keeping them in one file (or, if we use multiple files, name them e.g. infra_mixins.py, manage_mixins.py, etc). I had Claude do an analysis of the query parameter usage in infra and manage where he read the schema and reported on the percentage sharing of query params across all endpoints, max query params used by endpoints, etc.
Claude found zero overlap between infra and manage, so these should probably be separate files:
https://github.com/CiscoDevNet/ansible-nd/blob/rest_send_integration/ANALYSIS_ENDPOINT_PARAMETERS.md
There was a problem hiding this comment.
Another conclusion Claude made is that there are several mixins defined in mixins.py that are not used in either infra or manage schemas. These are probably legacy mixins from NDFC, so I'll open an issue to remove them. I'll double check these though...
"Key Finding: Only 3 of 11 defined mixins are actually used (LoginIdMixin, FabricNameMixin, ForceShowRunMixin). The 8 unused mixins (ClusterNameMixin, HealthCategoryMixin, InclAllMsdSwitchesMixin, LinkUuidMixin, NetworkNameMixin, NodeNameMixin, SwitchSerialNumberMixin, VrfNameMixin) have no corresponding parameters in either schema."
There was a problem hiding this comment.
As discussed in the call I think for a bunch of the files such as mixins, enums, types, ... they should have a scope and we could have them at different scopes here these mixins seems to be endpoints scoped but if we see later the need to move them to a larger scope we could once they are needed elsewhere.
If we already think some will be used elsewhere we should move these into the higher scope sooner.
There was a problem hiding this comment.
Thanks @lhercot and agree. I think the current scope for mixins.py seems appropriate for now.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pydantic_compat was moved into module_utils/commmon in nd42_rest_send branch. nd42_smart_endpoints and nd42_rest_send will be merged into a common integration branch shortly, so am updating at least some of the imports that are known to have changed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forgot to add this in the earlier commit.
typing.Final was added in Python 3.8. Since all affected files already use from __future__ import annotations (PEP 563), Final is only needed during static type checking and never evaluated at runtime. Guarding the import with TYPE_CHECKING avoids the ImportError on Python 3.7 with no dependency on typing_extensions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adding for Ansible sanity tests.
Literal was added in Python 3.8. ansible-test sanity runs against Python 3.7, so importing it directly causes ImportError. Since all three files already have `from __future__ import annotations`, all annotations are lazy strings at runtime and the TYPE_CHECKING guard is safe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ansible-test sanity requires all __init__.py files under plugins/ to be completely empty. No imports are broken since all consumers already import directly from submodules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| @@ -0,0 +1,89 @@ | |||
| # Copyright: (c) 2026, Allen Robel (@arobel) <arobel@cisco.com> | |||
There was a problem hiding this comment.
As discussed in the call I think for a bunch of the files such as mixins, enums, types, ... they should have a scope and we could have them at different scopes here these mixins seems to be endpoints scoped but if we see later the need to move them to a larger scope we could once they are needed elsewhere.
If we already think some will be used elsewhere we should move these into the higher scope sooner.
| ) | ||
|
|
||
|
|
||
| class QueryParams(ABC): |
There was a problem hiding this comment.
Shouldn't it be used in L#83, L#145 and L#225 to make sure they all respect the interface?
|
|
||
| @field_validator("sort") | ||
| @classmethod | ||
| def validate_sort(cls, value): |
There was a problem hiding this comment.
should this be an internal method? Do we expect to use it externally?
|
|
||
| def is_empty(self) -> bool: | ||
| """Check if any filter parameters are set.""" | ||
| return all(v is None for v in self.model_dump().values()) |
There was a problem hiding this comment.
Wouldn't using the following be more efficient?
| return all(v is None for v in self.model_dump().values()) | |
| return all(v is None for v in self.model_dump(exclude_none=True).values()) |
| """ | ||
|
|
||
| def __init__(self) -> None: | ||
| self._param_groups: list[Union[EndpointQueryParams, LuceneQueryParams]] = [] |
There was a problem hiding this comment.
Why is this not just list[QueryParams] as it defines your required interface while EndpointQueryParams and LuceneQueryParams are implementation of that interface?
This would require to be modified every time we add a QueryParams type which your interface definition is suppose to avoid.
| COMMON_CONFIG = ConfigDict(validate_assignment=True) | ||
|
|
||
|
|
||
| class _EpInfraAaaLocalUsersBase(LoginIdMixin, BaseModel): |
There was a problem hiding this comment.
I think this should be inheriting from NDEndpointBaseModel similar to this from Gaspard's PR: https://github.com/gmicol/ansible-nd/blob/6e81ee0f97a0ebaf84b1a811d28ba27c32f909ed/plugins/module_utils/endpoints/base.py#L20 so we can define specific behavior we want globally. The set_identifier part can be omited for now but the inheritance to a class we control will allow this facility to be added more easily in Gaspard's PR.
This will also help force the definition of:
api_version: Literal["v1"] = Field(default="v1", description="ND API version for this endpoint")
min_controller_version: str = Field(default="3.0.0", description="Minimum ND version supporting this endpoint")
class_name: Literal["EpInfraLoginPost"] = Field(default="EpInfraLoginPost", description="Class name for backward compatibility")
So developer won't be able to write endpoints without defining them or getting a global definition when it makes sense.
| return BasePath.nd_infra_aaa("localUsers") | ||
|
|
||
|
|
||
| class EpInfraAaaLocalUsersGet(_EpInfraAaaLocalUsersBase): |
There was a problem hiding this comment.
Should we keep v1 in the name? Is there places where we would import both v1 and v2 and have to rename the import or is this more an exception?
| COMMON_CONFIG = ConfigDict(validate_assignment=True) | ||
|
|
||
|
|
||
| class ClusterHealthConfigEndpointParams(EndpointQueryParams): |
There was a problem hiding this comment.
Shouldn't this use composition using ClusterNameMixin?
Same comment for all following EndpointParams that use elements that are already defined in mixins.
| API: Final = ApiPath.INFRA.value | ||
|
|
||
| @classmethod | ||
| def nd_infra(cls, *segments: str) -> str: |
There was a problem hiding this comment.
I think this should be a more generic method such as path or build_path.
| @classmethod | ||
| def nd_infra_aaa(cls, *segments: str) -> str: | ||
| """ | ||
| # Summary | ||
|
|
||
| Build ND infra AAA API path. | ||
|
|
||
| ## Parameters | ||
|
|
||
| - segments: Path segments to append after aaa (e.g., "localUsers") | ||
|
|
||
| ## Returns | ||
|
|
||
| - Complete ND infra AAA path | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| path = BasePath.nd_infra_aaa("localUsers") | ||
| # Returns: /api/v1/infra/aaa/localUsers | ||
| ``` | ||
| """ | ||
| return cls.nd_infra("aaa", *segments) | ||
|
|
||
| @classmethod | ||
| def nd_infra_clusterhealth(cls, *segments: str) -> str: | ||
| """ | ||
| # Summary | ||
|
|
||
| Build ND infra clusterhealth API path. | ||
|
|
||
| ## Parameters | ||
|
|
||
| - segments: Path segments to append after clusterhealth (e.g., "config", "status") | ||
|
|
||
| ## Returns | ||
|
|
||
| - Complete ND infra clusterhealth path | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| path = BasePath.nd_infra_clusterhealth("config") | ||
| # Returns: /api/v1/infra/clusterhealth/config | ||
| ``` | ||
| """ | ||
| return cls.nd_infra("clusterhealth", *segments) |
There was a problem hiding this comment.
I think this should be moved into their own BasePath class in a separate file infra_xxx.py with the current infra_xxx.py splitted by endpoint as mentioned in the other comment.
This will allow simpler imports and usage down the path as each endpoint will only have to import their parent path and call BasePath.build_path().
It will also allow better separation between each path in case changes need to be made and avoid having merge conflicts on base_paths_infra.py everytime we need to add a new path.
Summary
Adds the Smart Endpoints framework — a type-safe, Pydantic-validated approach to constructing ND REST API endpoint paths and query parameters. This PR is one of three companion PRs targeting
nd42_integration:nd42_rest_sendenums.py, shared test infrastructurend42_loggingLogclass and logging confignd42_smart_endpointsPlugin files added
Base classes (
plugins/module_utils/endpoints/)base_path.pyApiPath— string enum providing type-safe base API path constants shared across all endpoint versionsendpoint_mixins.pyLoginIdMixinfor endpoints requiring a login ID)query_params.pyEndpointQueryParams,LuceneQueryParams,CompositeQueryParams— Pydantic models for structured, validated query string constructionND API v1 endpoints (
plugins/module_utils/endpoints/v1/)base_paths_infra.py/api/v1/infranamespacebase_paths_manage.py/api/v1/managenamespaceinfra_aaa.pyinfra_clusterhealth.pyinfra_login.pyEpInfraLoginPost) for/api/v1/infra/loginmanage_switches.pyUnit tests added
test_base_path.pyApiPathbase classtest_base_paths_infra.pytest_base_paths_manage.pytest_endpoint_mixins.pyLoginIdMixinand other mixinstest_endpoints_api_v1_infra_aaa.pytest_endpoints_api_v1_infra_clusterhealth.pytest_endpoints_api_v1_infra_login.pytest_endpoints_api_v1_manage_switches.pytest_query_params.pyDependencies
plugins/module_utils/enums.py—HttpVerbEnum,BooleanStringEnum(fromnd42_rest_sendAdd RestSend framework, enums, and shared unit test infrastructure #185)plugins/module_utils/pydantic_compat.py— Pydantic shim (fromnd42_rest_sendAdd RestSend framework, enums, and shared unit test infrastructure #185)tests/unit/module_utils/common_utils.py—does_not_raise()(fromnd42_rest_sendAdd RestSend framework, enums, and shared unit test infrastructure #185)Merge order
nd42_rest_send(Add RestSend framework, enums, and shared unit test infrastructure #185) →nd42_integrationnd42_logging(Add Log class and logging config #184) →nd42_integrationnd42_integrationTest plan
nd42_integration:python -m pytest tests/unit/module_utils/endpoints/passes cleanlytox -e linterspasses (black, isort, pylint, mypy)ansible-test sanity --dockerpasses🤖 Generated with Claude Code