From 99bdb1a927109fe69c0dd133263c905547486941 Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Wed, 14 Oct 2026 15:58:20 +0530 Subject: [PATCH 1/2] Expose all generated resources on ManagementClient ManagementClient and AsyncManagementClient were missing nine resource properties that the generated Auth0 client already has: agents, client_profiles, connection_profiles, events, experimentation, groups, rate_limit_policies, risk_assessments, and user_attribute_profiles. Calls to those resources raised AttributeError on the wrapper. Add typed properties for each one to both classes. Add tests that compare the public properties of Auth0 with ManagementClient, and of AsyncAuth0 with AsyncManagementClient, so a missing resource fails CI. --- src/auth0/management/management_client.py | 81 ++++++++++++++++++++++ tests/management/test_management_client.py | 36 ++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/auth0/management/management_client.py b/src/auth0/management/management_client.py index 421f9bda..cd831a7c 100644 --- a/src/auth0/management/management_client.py +++ b/src/auth0/management/management_client.py @@ -61,19 +61,25 @@ def CustomDomainHeader(domain: str) -> RequestOptions: if TYPE_CHECKING: from .actions.client import ActionsClient, AsyncActionsClient + from .agents.client import AsyncAgentsClient, AgentsClient from .anomaly.client import AnomalyClient, AsyncAnomalyClient from .attack_protection.client import AsyncAttackProtectionClient, AttackProtectionClient from .branding.client import AsyncBrandingClient, BrandingClient from .client_grants.client import AsyncClientGrantsClient, ClientGrantsClient + from .client_profiles.client import AsyncClientProfilesClient, ClientProfilesClient from .clients.client import AsyncClientsClient, ClientsClient + from .connection_profiles.client import AsyncConnectionProfilesClient, ConnectionProfilesClient from .connections.client import AsyncConnectionsClient, ConnectionsClient from .custom_domains.client import AsyncCustomDomainsClient, CustomDomainsClient from .device_credentials.client import AsyncDeviceCredentialsClient, DeviceCredentialsClient from .email_templates.client import AsyncEmailTemplatesClient, EmailTemplatesClient from .emails.client import AsyncEmailsClient, EmailsClient from .event_streams.client import AsyncEventStreamsClient, EventStreamsClient + from .events.client import AsyncEventsClient, EventsClient + from .experimentation.client import AsyncExperimentationClient, ExperimentationClient from .flows.client import AsyncFlowsClient, FlowsClient from .forms.client import AsyncFormsClient, FormsClient + from .groups.client import AsyncGroupsClient, GroupsClient from .guardian.client import AsyncGuardianClient, GuardianClient from .hooks.client import AsyncHooksClient, HooksClient from .jobs.client import AsyncJobsClient, JobsClient @@ -83,8 +89,10 @@ def CustomDomainHeader(domain: str) -> RequestOptions: from .network_acls.client import AsyncNetworkAclsClient, NetworkAclsClient from .organizations.client import AsyncOrganizationsClient, OrganizationsClient from .prompts.client import AsyncPromptsClient, PromptsClient + from .rate_limit_policies.client import AsyncRateLimitPoliciesClient, RateLimitPoliciesClient from .refresh_tokens.client import AsyncRefreshTokensClient, RefreshTokensClient from .resource_servers.client import AsyncResourceServersClient, ResourceServersClient + from .risk_assessments.client import AsyncRiskAssessmentsClient, RiskAssessmentsClient from .roles.client import AsyncRolesClient, RolesClient from .rules.client import AsyncRulesClient, RulesClient from .rules_configs.client import AsyncRulesConfigsClient, RulesConfigsClient @@ -95,6 +103,7 @@ def CustomDomainHeader(domain: str) -> RequestOptions: from .tenants.client import AsyncTenantsClient, TenantsClient from .tickets.client import AsyncTicketsClient, TicketsClient from .token_exchange_profiles.client import AsyncTokenExchangeProfilesClient, TokenExchangeProfilesClient + from .user_attribute_profiles.client import AsyncUserAttributeProfilesClient, UserAttributeProfilesClient from .user_blocks.client import AsyncUserBlocksClient, UserBlocksClient from .user_grants.client import AsyncUserGrantsClient, UserGrantsClient from .users.client import AsyncUsersClient, UsersClient @@ -221,6 +230,10 @@ def __init__( def actions(self) -> "ActionsClient": return self._api.actions + @property + def agents(self) -> "AgentsClient": + return self._api.agents + @property def anomaly(self) -> "AnomalyClient": return self._api.anomaly @@ -237,10 +250,18 @@ def branding(self) -> "BrandingClient": def client_grants(self) -> "ClientGrantsClient": return self._api.client_grants + @property + def client_profiles(self) -> "ClientProfilesClient": + return self._api.client_profiles + @property def clients(self) -> "ClientsClient": return self._api.clients + @property + def connection_profiles(self) -> "ConnectionProfilesClient": + return self._api.connection_profiles + @property def connections(self) -> "ConnectionsClient": return self._api.connections @@ -265,6 +286,14 @@ def emails(self) -> "EmailsClient": def event_streams(self) -> "EventStreamsClient": return self._api.event_streams + @property + def events(self) -> "EventsClient": + return self._api.events + + @property + def experimentation(self) -> "ExperimentationClient": + return self._api.experimentation + @property def flows(self) -> "FlowsClient": return self._api.flows @@ -273,6 +302,10 @@ def flows(self) -> "FlowsClient": def forms(self) -> "FormsClient": return self._api.forms + @property + def groups(self) -> "GroupsClient": + return self._api.groups + @property def guardian(self) -> "GuardianClient": return self._api.guardian @@ -309,6 +342,10 @@ def organizations(self) -> "OrganizationsClient": def prompts(self) -> "PromptsClient": return self._api.prompts + @property + def rate_limit_policies(self) -> "RateLimitPoliciesClient": + return self._api.rate_limit_policies + @property def refresh_tokens(self) -> "RefreshTokensClient": return self._api.refresh_tokens @@ -317,6 +354,10 @@ def refresh_tokens(self) -> "RefreshTokensClient": def resource_servers(self) -> "ResourceServersClient": return self._api.resource_servers + @property + def risk_assessments(self) -> "RiskAssessmentsClient": + return self._api.risk_assessments + @property def roles(self) -> "RolesClient": return self._api.roles @@ -357,6 +398,10 @@ def tickets(self) -> "TicketsClient": def token_exchange_profiles(self) -> "TokenExchangeProfilesClient": return self._api.token_exchange_profiles + @property + def user_attribute_profiles(self) -> "UserAttributeProfilesClient": + return self._api.user_attribute_profiles + @property def user_blocks(self) -> "UserBlocksClient": return self._api.user_blocks @@ -497,6 +542,10 @@ def __init__( def actions(self) -> "AsyncActionsClient": return self._api.actions + @property + def agents(self) -> "AsyncAgentsClient": + return self._api.agents + @property def anomaly(self) -> "AsyncAnomalyClient": return self._api.anomaly @@ -513,10 +562,18 @@ def branding(self) -> "AsyncBrandingClient": def client_grants(self) -> "AsyncClientGrantsClient": return self._api.client_grants + @property + def client_profiles(self) -> "AsyncClientProfilesClient": + return self._api.client_profiles + @property def clients(self) -> "AsyncClientsClient": return self._api.clients + @property + def connection_profiles(self) -> "AsyncConnectionProfilesClient": + return self._api.connection_profiles + @property def connections(self) -> "AsyncConnectionsClient": return self._api.connections @@ -541,6 +598,14 @@ def emails(self) -> "AsyncEmailsClient": def event_streams(self) -> "AsyncEventStreamsClient": return self._api.event_streams + @property + def events(self) -> "AsyncEventsClient": + return self._api.events + + @property + def experimentation(self) -> "AsyncExperimentationClient": + return self._api.experimentation + @property def flows(self) -> "AsyncFlowsClient": return self._api.flows @@ -549,6 +614,10 @@ def flows(self) -> "AsyncFlowsClient": def forms(self) -> "AsyncFormsClient": return self._api.forms + @property + def groups(self) -> "AsyncGroupsClient": + return self._api.groups + @property def guardian(self) -> "AsyncGuardianClient": return self._api.guardian @@ -585,6 +654,10 @@ def organizations(self) -> "AsyncOrganizationsClient": def prompts(self) -> "AsyncPromptsClient": return self._api.prompts + @property + def rate_limit_policies(self) -> "AsyncRateLimitPoliciesClient": + return self._api.rate_limit_policies + @property def refresh_tokens(self) -> "AsyncRefreshTokensClient": return self._api.refresh_tokens @@ -593,6 +666,10 @@ def refresh_tokens(self) -> "AsyncRefreshTokensClient": def resource_servers(self) -> "AsyncResourceServersClient": return self._api.resource_servers + @property + def risk_assessments(self) -> "AsyncRiskAssessmentsClient": + return self._api.risk_assessments + @property def roles(self) -> "AsyncRolesClient": return self._api.roles @@ -633,6 +710,10 @@ def tickets(self) -> "AsyncTicketsClient": def token_exchange_profiles(self) -> "AsyncTokenExchangeProfilesClient": return self._api.token_exchange_profiles + @property + def user_attribute_profiles(self) -> "AsyncUserAttributeProfilesClient": + return self._api.user_attribute_profiles + @property def user_blocks(self) -> "AsyncUserBlocksClient": return self._api.user_blocks diff --git a/tests/management/test_management_client.py b/tests/management/test_management_client.py index 7901a4d5..9ed5fc9b 100644 --- a/tests/management/test_management_client.py +++ b/tests/management/test_management_client.py @@ -13,6 +13,7 @@ ManagementClient, TokenProvider, ) +from auth0.management.client import AsyncAuth0, Auth0 from auth0.management.management_client import ( CUSTOM_DOMAIN_HEADER as _CUSTOM_DOMAIN_HEADER, ) @@ -168,19 +169,25 @@ def test_has_all_expected_properties(self, client): """Should have all expected sub-client properties.""" expected_properties = [ "actions", + "agents", "anomaly", "attack_protection", "branding", "client_grants", + "client_profiles", "clients", + "connection_profiles", "connections", "custom_domains", "device_credentials", "email_templates", "emails", "event_streams", + "events", + "experimentation", "flows", "forms", + "groups", "guardian", "hooks", "jobs", @@ -190,8 +197,10 @@ def test_has_all_expected_properties(self, client): "network_acls", "organizations", "prompts", + "rate_limit_policies", "refresh_tokens", "resource_servers", + "risk_assessments", "roles", "rules", "rules_configs", @@ -202,6 +211,7 @@ def test_has_all_expected_properties(self, client): "tenants", "tickets", "token_exchange_profiles", + "user_attribute_profiles", "user_blocks", "user_grants", "users", @@ -256,6 +266,32 @@ def test_init_with_custom_client_info(self): assert decoded == custom_info +class TestManagementClientMatchesGeneratedClient: + """Keep the hand-written wrappers in step with the generated clients.""" + + @staticmethod + def _public_properties(cls): + return { + name + for name, value in vars(cls).items() + if isinstance(value, property) and not name.startswith("_") + } + + def test_management_client_exposes_every_generated_resource(self): + """Should expose every resource the generated Auth0 client has.""" + missing = self._public_properties(Auth0) - self._public_properties( + ManagementClient + ) + assert missing == set(), f"ManagementClient is missing: {sorted(missing)}" + + def test_async_management_client_exposes_every_generated_resource(self): + """Should expose every resource the generated AsyncAuth0 client has.""" + missing = self._public_properties(AsyncAuth0) - self._public_properties( + AsyncManagementClient + ) + assert missing == set(), f"AsyncManagementClient is missing: {sorted(missing)}" + + class TestTokenProvider: """Tests for TokenProvider.""" From b5871ee24121a4c38c3dc9464eb5996c45637afe Mon Sep 17 00:00:00 2001 From: Kunal Dawar Date: Wed, 14 Oct 2026 16:00:54 +0530 Subject: [PATCH 2/2] Explain manual wrapper upkeep and name missing resources in drift test Add a header comment to management_client.py describing how to keep it in step with the generated client. The drift test failure message now says what to add and where. --- src/auth0/management/management_client.py | 4 ++++ tests/management/test_management_client.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/auth0/management/management_client.py b/src/auth0/management/management_client.py index cd831a7c..82c5cd1c 100644 --- a/src/auth0/management/management_client.py +++ b/src/auth0/management/management_client.py @@ -1,3 +1,7 @@ +# This file is hand-written and listed in .fernignore, so Fern does not overwrite it. +# Each resource that the generated Auth0 client exposes needs a matching property +# on ManagementClient and AsyncManagementClient, plus a TYPE_CHECKING import. +# tests/management/test_management_client.py fails and lists any missing resource. from __future__ import annotations import base64 diff --git a/tests/management/test_management_client.py b/tests/management/test_management_client.py index 9ed5fc9b..766129eb 100644 --- a/tests/management/test_management_client.py +++ b/tests/management/test_management_client.py @@ -282,14 +282,20 @@ def test_management_client_exposes_every_generated_resource(self): missing = self._public_properties(Auth0) - self._public_properties( ManagementClient ) - assert missing == set(), f"ManagementClient is missing: {sorted(missing)}" + assert missing == set(), ( + f"ManagementClient is missing: {sorted(missing)}. Add a property and a " + "TYPE_CHECKING import for each one in management_client.py." + ) def test_async_management_client_exposes_every_generated_resource(self): """Should expose every resource the generated AsyncAuth0 client has.""" missing = self._public_properties(AsyncAuth0) - self._public_properties( AsyncManagementClient ) - assert missing == set(), f"AsyncManagementClient is missing: {sorted(missing)}" + assert missing == set(), ( + f"AsyncManagementClient is missing: {sorted(missing)}. Add a property and " + "a TYPE_CHECKING import for each one in management_client.py." + ) class TestTokenProvider: