Repository: auth0/auth0-python
Pull request #696 (2025-06-04 14:24:40)

chore: merge community PRs – bugfixes, features, and dependency upgrades
### Changes

This PR consolidates multiple community contributions and dependency upgrade:

- Features
   - [Self Service Profiles Support ](https://github.com/auth0/auth0-python/pull/673) ([nzetzl](https://github.com/nzetzl))

- Fixes
   - [Adding Global Seed](https://github.com/auth0/auth0-python/pull/690) [(allrob23)](https://github.com/allrob23)

- Bumping Dependencies


### Testing

- [x] This change adds test coverage

- [x] This change has been tested on the latest version of the platform/language or why not

### Contributor Checklist

- [x] I agree to adhere to the [Auth0 General Contribution Guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md).
- [x] I agree to uphold the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).
- [x] All existing and new tests complete without errors



Patch:
diff --git a/README.md b/README.md
index 13200c2..1efbcfa 100644
--- a/README.md
+++ b/README.md
@@ -122,6 +122,7 @@ For more code samples on how to integrate the auth0-python SDK in your Python ap
 - Roles() ( `Auth0().roles` )
 - RulesConfigs() ( `Auth0().rules_configs` )
 - Rules() ( `Auth0().rules` )
+- SelfServiceProfiles() ( `Auth0().self_service_profiles` )
 - Stats() ( `Auth0().stats` )
 - Tenants() ( `Auth0().tenants` )
 - Tickets() ( `Auth0().tickets` )
diff --git a/auth0/management/__init__.py b/auth0/management/__init__.py
index d6fee4b..62b1e8a 100644
--- a/auth0/management/__init__.py
+++ b/auth0/management/__init__.py
@@ -22,6 +22,7 @@ from .resource_servers import ResourceServers
 from .roles import Roles
 from .rules import Rules
 from .rules_configs import RulesConfigs
+from .self_service_profiles import SelfServiceProfiles
 from .stats import Stats
 from .tenants import Tenants
 from .tickets import Tickets
@@ -59,6 +60,7 @@ __all__ = (
     "Roles",
     "RulesConfigs",
     "Rules",
+    "SelfServiceProfiles",
     "Stats",
     "Tenants",
     "Tickets",
diff --git a/auth0/management/auth0.py b/auth0/management/auth0.py
index 2879a9e..5615f86 100644
--- a/auth0/management/auth0.py
+++ b/auth0/management/auth0.py
@@ -26,6 +26,7 @@ from .resource_servers import ResourceServers
 from .roles import Roles
 from .rules import Rules
 from .rules_configs import RulesConfigs
+from .self_service_profiles import SelfServiceProfiles
 from .stats import Stats
 from .tenants import Tenants
 from .tickets import Tickets
@@ -86,6 +87,9 @@ class Auth0:
         self.roles = Roles(domain, token, rest_options=rest_options)
         self.rules_configs = RulesConfigs(domain, token, rest_options=rest_options)
         self.rules = Rules(domain, token, rest_options=rest_options)
+        self.self_service_profiles = SelfServiceProfiles(
+            domain, token, rest_options=rest_options
+        )
         self.stats = Stats(domain, token, rest_options=rest_options)
         self.tenants = Tenants(domain, token, rest_options=rest_options)
         self.tickets = Tickets(domain, token, rest_options=rest_options)
diff --git a/auth0/management/self_service_profiles.py b/auth0/management/self_service_profiles.py
new file mode 100644
index 0000000..a9a5261
--- /dev/null
+++ b/auth0/management/self_service_profiles.py
@@ -0,0 +1,180 @@
+from __future__ import annotations
+
+from typing import Any, List  # List is being used as list is already a method.
+
+from ..rest import RestClient, RestClientOptions
+from ..types import TimeoutType
+
+
+class SelfServiceProfiles:
+    """Auth0 Self Service Profiles endpoints
+
+    Args:
+        domain (str): Your Auth0 domain, e.g: 'username.auth0.com'
+
+        token (str): Management API v2 Token
+
+        telemetry (bool, optional): Enable or disable Telemetry
+            (defaults to True)
+
+        timeout (float or tuple, optional): Change the requests
+            connect and read timeout. Pass a tuple to specify
+            both values separately or a float to set both to it.
+            (defaults to 5.0 for both)
+
+        protocol (str, optional): Protocol to use when making requests.
+            (defaults to "https")
+
+        rest_options (RestClientOptions): Pass an instance of
+            RestClientOptions to configure additional RestClient
+            options, such as rate-limit retries.
+            (defaults to None)
+    """
+
+    def __init__(
+        self,
+        domain: str,
+        token: str,
+        telemetry: bool = True,
+        timeout: TimeoutType = 5.0,
+        protocol: str = "https",
+        rest_options: RestClientOptions | None = None,
+    ) -> None:
+        self.domain = domain
+        self.protocol = protocol
+        self.client = RestClient(
+            jwt=token, telemetry=telemetry, timeout=timeout, options=rest_options
+        )
+
+    def _url(self, profile_id: str | None = None) -> str:
+        url = f"{self.protocol}://{self.domain}/api/v2/self-service-profiles"
+        if profile_id is not None:
+            return f"{url}/{profile_id}"
+        return url
+
+    def all(
+        self,
+        page: int = 0,
+        per_page: int = 25,
+        include_totals: bool = True,
+    ) -> List[dict[str, Any]]:
+        """List self-service profiles.
+
+        Args:
+            page (int, optional): The result's page number (zero based). By default,
+                retrieves the first page of results.
+
+            per_page (int, optional): The amount of entries per page. By default,
+                retrieves 25 results per page.
+
+            include_totals (bool, optional): True if the query summary is
+                to be included in the result, False otherwise. Defaults to True.
+
+        See: https://auth0.com/docs/api/management/v2/self-service-profiles/get-self-service-profiles
+        """
+
+        params = {
+            "page": page,
+            "per_page": per_page,
+            "include_totals": str(include_totals).lower(),
+        }
+
+        return self.client.get(self._url(), params=params)
+
+    def create(self, body: dict[str, Any]) -> dict[str, Any]:
+        """Create a new self-service profile.
+
+        Args:
+            body (dict): Attributes for the new self-service profile.
+
+        See: https://auth0.com/docs/api/management/v2/self-service-profiles/post-self-service-profiles
+        """
+
+        return self.client.post(self._url(), data=body)
+
+    def get(self, profile_id: str) -> dict[str, Any]:
+        """Get a self-service profile.
+
+        Args:
+            id (str): The id of the self-service profile to retrieve.
+
+        See: https://auth0.com/docs/api/management/v2/self-service-profiles/get-self-service-profiles-by-id
+        """
+
+        return self.client.get(self._url(profile_id))
+
+    def delete(self, profile_id: str) -> None:
+        """Delete a self-service profile.
+
+        Args:
+            id (str): The id of the self-service profile to delete.
+
+        See: https://auth0.com/docs/api/management/v2/self-service-profiles/delete-self-service-profiles-by-id
+        """
+
+        self.client.delete(self._url(profile_id))
+
+    def update(self, profile_id: str, body: dict[str, Any]) -> dict[str, Any]:
+        """Update a self-service profile.
+
+        Args:
+            id (str): The id of the self-service profile to update.
+
+            body (dict): Attributes of the self-service profile to modify.
+
+        See: https://auth0.com/docs/api/management/v2/self-service-profiles/patch-self-service-profiles-by-id
+        """
+
+        return self.client.patch(self._url(profile_id), data=body)
+
+    def get_custom_text(
+        self, profile_id: str, language: str, page: str
+    ) -> dict[str, Any]:
+        """Get the custom text for a self-service profile.
+
+        See: https://auth0.com/docs/api/management/v2/self-service-profiles/get-self-service-profile-custom-text
+        """
+
+        url = self._url(f"{profile_id}/custom-text/{language}/{page}")
+        return self.client.get(url)
+
+    def update_custom_text(
+        self, profile_id: str, language: str, page: str, body: dict[str, Any]
+    ) -> dict[str, Any]:
+        """Update the custom text for a self-service profile.
+
+        See: https://auth0.com/docs/api/management/v2/self-service-profiles/put-self-service-profile-custom-text
+        """
+
+        url = self._url(f"{profile_id}/custom-text/{language}/{page}")
+        return self.client.put(url, data=body)
+
+    def create_sso_ticket(
+        self, profile_id: str, body: dict[str, Any]
+    ) -> dict[str, Any]:
+        """Create a single sign-on ticket for a self-service profile.
+
+        Args:
+            id (str): The id of the self-service profile to create the ticket for.
+
+            body (dict): Attributes for the single sign-on ticket.
+
+        See: https://auth0.com/docs/api/management/v2/self-service-profiles/post-sso-ticket
+        """
+
+        url = self._url(f"{profile_id}/sso-ticket")
+        return self.client.post(url, data=body)
+
+    def revoke_sso_ticket(self, profile_id: str, ticket_id: str) -> None:
+        """Revoke a single sign-on ticket for a self-service profile.
+
+        Args:
+            id (str): The id of the self-service profile to revoke the ticket from.
+
+            ticket (str): The ticket to revoke.
+
+        See: https://auth0.com/docs/api/management/v2/self-service-profiles/post-revoke
+        """
+
+        url = self._url(f"{profile_id}/sso-ticket/{ticket_id}/revoke")
+        self.client.post(url)
\ No newline at end of file
diff --git a/docs/source/management.rst b/docs/source/management.rst
index e928f00..8bccaa2 100644
--- a/docs/source/management.rst
+++ b/docs/source/management.rst
@@ -177,6 +177,14 @@ management.rules module
    :undoc-members:
    :show-inheritance:
 
+management.self_service_profiles module
+-----------------------------------------
+
+.. automodule:: auth0.management.self_service_profiles
+   :members:
+   :undoc-members:
+   :show-inheritance:
+   
 management.stats module
 --------------------------
 
diff --git a/requirements.txt b/requirements.txt
index 0f57872..0eab0cf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
 aiohttp==3.8.6 ; python_version >= "3.7" and python_version < "4.0"
 aioresponses==0.7.4 ; python_version >= "3.7" and python_version < "4.0"
-aiosignal==1.3.1 ; python_version >= "3.7" and python_version < "4.0"
+aiosignal==1.3.2 ; python_version >= "3.7" and python_version < "4.0"
 argcomplete==3.5.3 ; python_version >= "3.7" and python_version < "4.0"
 async-timeout==4.0.3 ; python_version >= "3.7" and python_version < "4.0"
 asynctest==0.13.0 ; python_version >= "3.7" and python_version < "3.8"
@@ -8,7 +8,7 @@ attrs==23.1.0 ; python_version >= "3.7" and python_version < "4.0"
 certifi==2025.1.31 ; python_version >= "3.7" and python_version < "4.0"
 cffi==1.17.1 ; python_version >= "3.7" and python_version < "4.0"
 charset-normalizer==3.2.0 ; python_version >= "3.7" and python_version < "4.0"
-click==8.1.7 ; python_version >= "3.7" and python_version < "4.0"
+click==8.1.8 ; python_version >= "3.7" and python_version < "4.0"
 colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows"
 coverage[toml]==7.2.7 ; python_version >= "3.7" and python_version < "4.0"
 cryptography==44.0.1 ; python_version >= "3.7" and python_version < "4.0"
@@ -23,7 +23,7 @@ packaging==23.1 ; python_version >= "3.7" and python_version < "4.0"
 pipx==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
 pluggy==1.2.0 ; python_version >= "3.7" and python_version < "4.0"
 pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0"
-pyjwt==2.8.0 ; python_version >= "3.7" and python_version < "4.0"
+pyjwt==2.9.0 ; python_version >= "3.7" and python_version < "4.0"
 pyopenssl==25.0.0 ; python_version >= "3.7" and python_version < "4.0"
 pytest-aiohttp==1.0.4 ; python_version >= "3.7" and python_version < "4.0"
 pytest-asyncio==0.23.8 ; python_version >= "3.7" and python_version < "4.0"
@@ -37,5 +37,5 @@ types-pyyaml==6.0.12.11 ; python_version >= "3.7" and python_version < "4.0"
 typing-extensions==4.7.1 ; python_version >= "3.7" and python_version < "3.8"
 urllib3==2.2.2 ; python_version >= "3.7" and python_version < "4.0"
 userpath==1.9.0 ; python_version >= "3.7" and python_version < "4.0"
-yarl==1.9.2 ; python_version >= "3.7" and python_version < "4.0"
+yarl==1.20.0 ; python_version >= "3.7" and python_version < "4.0"
 zipp==3.19.1 ; python_version >= "3.7" and python_version < "3.8"


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8db7e52..ed0f2e9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -80,6 +80,6 @@ jobs:
 
       - if: ${{ matrix.python-version == '3.10' }}
         name: Upload coverage
-        uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # pin@5.4.2
+        uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # pin@5.4.3
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
\ No newline at end of file
diff --git a/auth0/test/conftest.py b/auth0/test/conftest.py
new file mode 100644
index 0000000..1247142
--- /dev/null
+++ b/auth0/test/conftest.py
@@ -0,0 +1,7 @@
+import pytest
+import random
+
+@pytest.fixture(autouse=True)
+def set_random_seed():
+    random.seed(42)
+    print("Random seeded to 42")
\ No newline at end of file
diff --git a/auth0/test/management/test_self_service_profiles.py b/auth0/test/management/test_self_service_profiles.py
new file mode 100644
index 0000000..0bc6fb3
--- /dev/null
+++ b/auth0/test/management/test_self_service_profiles.py
@@ -0,0 +1,124 @@
+import unittest
+from unittest import mock
+
+from ...management.self_service_profiles import SelfServiceProfiles
+
+
+class TestSelfServiceProfiles(unittest.TestCase):
+    def test_init_with_optionals(self):
+        t = SelfServiceProfiles(
+            domain="domain", token="jwttoken", telemetry=False, timeout=(10, 2)
+        )
+        self.assertEqual(t.client.options.timeout, (10, 2))
+        telemetry_header = t.client.base_headers.get("Auth0-Client", None)
+        self.assertEqual(telemetry_header, None)
+
+    @mock.patch("auth0.management.self_service_profiles.RestClient")
+    def test_all(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = SelfServiceProfiles(domain="domain", token="jwttoken")
+        s.all()
+
+        mock_instance.get.assert_called_with(
+            "https://domain/api/v2/self-service-profiles",
+            params={"page": 0, "per_page": 25, "include_totals": "true"},
+        )
+
+        s.all(page=1, per_page=50, include_totals=False)
+
+        mock_instance.get.assert_called_with(
+            "https://domain/api/v2/self-service-profiles",
+            params={"page": 1, "per_page": 50, "include_totals": "false"},
+        )
+
+    @mock.patch("auth0.management.self_service_profiles.RestClient")
+    def test_create(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = SelfServiceProfiles(domain="domain", token="jwttoken")
+        s.create({"name": "test"})
+
+        mock_instance.post.assert_called_with(
+            "https://domain/api/v2/self-service-profiles", data={"name": "test"}
+        )
+
+    @mock.patch("auth0.management.self_service_profiles.RestClient")
+    def test_get(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = SelfServiceProfiles(domain="domain", token="jwttoken")
+        s.get("an-id")
+
+        mock_instance.get.assert_called_with(
+            "https://domain/api/v2/self-service-profiles/an-id"
+        )
+
+    @mock.patch("auth0.management.self_service_profiles.RestClient")
+    def test_delete(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = SelfServiceProfiles(domain="domain", token="jwttoken")
+        s.delete("an-id")
+
+        mock_instance.delete.assert_called_with(
+            "https://domain/api/v2/self-service-profiles/an-id"
+        )
+
+    @mock.patch("auth0.management.self_service_profiles.RestClient")
+    def test_update(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = SelfServiceProfiles(domain="domain", token="jwttoken")
+        s.update("an-id", {"a": "b", "c": "d"})
+
+        mock_instance.patch.assert_called_with(
+            "https://domain/api/v2/self-service-profiles/an-id",
+            data={"a": "b", "c": "d"},
+        )
+
+    @mock.patch("auth0.management.self_service_profiles.RestClient")
+    def test_get_custom_text(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = SelfServiceProfiles(domain="domain", token="jwttoken")
+        s.get_custom_text("an-id", "en", "page")
+
+        mock_instance.get.assert_called_with(
+            "https://domain/api/v2/self-service-profiles/an-id/custom-text/en/page"
+        )
+
+    @mock.patch("auth0.management.self_service_profiles.RestClient")
+    def test_update_custom_text(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = SelfServiceProfiles(domain="domain", token="jwttoken")
+        s.update_custom_text("an-id", "en", "page", {"a": "b", "c": "d"})
+
+        mock_instance.put.assert_called_with(
+            "https://domain/api/v2/self-service-profiles/an-id/custom-text/en/page",
+            data={"a": "b", "c": "d"},
+        )
+
+    @mock.patch("auth0.management.self_service_profiles.RestClient")
+    def test_create_sso_ticket(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = SelfServiceProfiles(domain="domain", token="jwttoken")
+        s.create_sso_ticket("an-id", {"a": "b", "c": "d"})
+
+        mock_instance.post.assert_called_with(
+            "https://domain/api/v2/self-service-profiles/an-id/sso-ticket",
+            data={"a": "b", "c": "d"},
+        )
+
+    @mock.patch("auth0.management.self_service_profiles.RestClient")
+    def test_revoke_sso_ticket(self, mock_rc):
+        mock_instance = mock_rc.return_value
+
+        s = SelfServiceProfiles(domain="domain", token="jwttoken")
+        s.revoke_sso_ticket("an-id", "ticket-id")
+
+        mock_instance.post.assert_called_with(
+            "https://domain/api/v2/self-service-profiles/an-id/sso-ticket/ticket-id/revoke"
+        )
\ No newline at end of file
diff --git a/auth0/test_async/conftest.py b/auth0/test_async/conftest.py
new file mode 100644
index 0000000..1247142
--- /dev/null
+++ b/auth0/test_async/conftest.py
@@ -0,0 +1,7 @@
+import pytest
+import random
+
+@pytest.fixture(autouse=True)
+def set_random_seed():
+    random.seed(42)
+    print("Random seeded to 42")
\ No newline at end of file