From 9e7e0f2978946e720c84150c837d621e8071c8f2 Mon Sep 17 00:00:00 2001 From: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:11:19 +0000 Subject: [PATCH] SDK regeneration --- CONTRIBUTING.md | 125 ++++++++++++ reference.md | 14 +- src/auth0/myorganization/__init__.py | 55 ++++-- src/auth0/myorganization/client.py | 12 ++ src/auth0/myorganization/core/http_client.py | 3 +- .../myorganization/core/http_sse/_api.py | 100 ++++++++-- .../myorganization/core/pydantic_utilities.py | 186 +++--------------- .../myorganization/core/serialization.py | 87 +++++++- tests/utils/test_http_client.py | 32 +++ tests/wire/conftest.py | 9 +- .../test_organization_identityProviders.py | 12 +- wiremock/docker-compose.test.yml | 2 +- wiremock/wiremock-mappings.json | 180 ++++++++--------- 13 files changed, 504 insertions(+), 313 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..af948ce --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- Python 3.9+ +- pip +- poetry + +### Installation + +Install the project dependencies: + +```bash +poetry install +``` + +### Building + +Build the project: + +```bash +poetry build +``` + +### Testing + +Run the test suite: + +```bash +poetry run pytest +``` + +### Linting and Formatting + +Check code style: + +```bash +poetry run ruff check . +poetry run ruff format . +``` + +### Type Checking + +Run the type checker: + +```bash +poetry run mypy . +``` + +## About Generated Code + +**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. + +### Generated Files + +The following directories contain generated code: +- `src/` - API client classes and types +- Most Python files in the project + +### How to Customize + +If you need to customize the SDK, you have two options: + +#### Option 1: Use `.fernignore` + +For custom code that should persist across SDK regenerations: + +1. Create a `.fernignore` file in the project root +2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) +3. Add your custom code to those files + +Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. + +For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). + +#### Option 2: Contribute to the Generator + +If you want to change how code is generated for all users of this SDK: + +1. The Python SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/python-v2/` +3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) +4. Submit a pull request with your changes to the generator + +This approach is best for: +- Bug fixes in generated code +- New features that would benefit all users +- Improvements to code generation patterns + +## Making Changes + +### Workflow + +1. Create a new branch for your changes +2. Make your modifications +3. Run tests to ensure nothing breaks: `poetry run pytest` +4. Run linting and formatting: `poetry run ruff check .` and `poetry run ruff format .` +5. Run type checking: `poetry run mypy .` +6. Build the project: `poetry build` +7. Commit your changes with a clear commit message +8. Push your branch and create a pull request + +### Commit Messages + +Write clear, descriptive commit messages that explain what changed and why. + +### Code Style + +This project uses automated code formatting and linting. Run `poetry run ruff format .` and `poetry run ruff check .` before committing to ensure your code meets the project's style guidelines. + +## Questions or Issues? + +If you have questions or run into issues: + +1. Check the [Fern documentation](https://buildwithfern.com) +2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) +3. Open a new issue if your question hasn't been addressed + +## License + +By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/reference.md b/reference.md index 906dad5..5b139a7 100644 --- a/reference.md +++ b/reference.md @@ -611,8 +611,14 @@ client = Auth0( client.organization.identity_providers.create( request=IdpOidcRequest( - name="oidcIdp", strategy="oidc", + options=IdpOidcOptionsRequest( + type="front_channel", + client_id="a8f3b2e7-5d1c-4f9a-8b0d-2e1c3a5b6f7d", + client_secret="KzQp2sVxR8nTgMjFhYcEWuLoIbDvUoC6A9B1zX7yWqFjHkGrP5sQdLmNp", + discovery_url="https://{yourDomain}/.well-known/openid-configuration", + ), + name="oidcIdp", domains=[ "mydomain.com" ], @@ -620,12 +626,6 @@ client.organization.identity_providers.create( show_as_button=True, assign_membership_on_login=False, is_enabled=True, - options=IdpOidcOptionsRequest( - type="front_channel", - client_id="a8f3b2e7-5d1c-4f9a-8b0d-2e1c3a5b6f7d", - client_secret="KzQp2sVxR8nTgMjFhYcEWuLoIbDvUoC6A9B1zX7yWqFjHkGrP5sQdLmNp", - discovery_url="https://{yourDomain}/.well-known/openid-configuration", - ), ), ) diff --git a/src/auth0/myorganization/__init__.py b/src/auth0/myorganization/__init__.py index 349b857..bc43d80 100644 --- a/src/auth0/myorganization/__init__.py +++ b/src/auth0/myorganization/__init__.py @@ -15,10 +15,10 @@ CreateIdentityProviderResponseContent, CreateIdpDomainResponseContent, CreateIdpProvisioningScimTokenResponseContent, + CreateMemberInvitationInvitee, CreateMemberInvitationResponseContent, CreateOrganizationDomainResponseContent, DomainIdp, - DomainVerificationEnum, ErrorResponseContent, FedMetadataXml, GetConfigurationResponseContent, @@ -29,6 +29,7 @@ GetOrganizationDetailsResponseContent, GetOrganizationDomainResponseContent, GetOrganizationMemberResponseContent, + GetOrganizationMemberRolesResponseContent, IdentityProviderConfigAdfs, IdentityProviderConfigGoogleApps, IdentityProviderConfigOidc, @@ -37,7 +38,6 @@ IdentityProviderConfigSamlp, IdentityProviderConfigWaad, IdentityProvidersConfig, - IdentityProvidersConfigDomainAlias, IdentityProvidersConfigEnabledFeaturesEnum, IdentityProvidersConfigOrganization, IdentityProvidersConfigProvisioningMethodsEnum, @@ -121,7 +121,10 @@ ListDomainIdentityProvidersResponseContent, ListIdentityProvidersResponseContent, ListIdpProvisioningScimTokensResponseContent, + ListMembersInvitationsResponseContent, ListOrganizationDomainsResponseContent, + ListOrganizationMembersResponseContent, + ListRolesResponseContent, Manual, MemberInvitation, MemberInvitationInvitee, @@ -130,6 +133,7 @@ OrgBranding, OrgBrandingColors, OrgDetails, + OrgDetailsRead, OrgDomain, OrgDomainId, OrgDomainName, @@ -137,9 +141,11 @@ OrgId, OrgMember, OrgMemberId, - OrgMemberRole, - OrgMemberRoleId, + OrgMemberIdReadOnly, OrganizationAccessLevelEnum, + OrganizationMemberRolesChangeRequestContent, + Role, + RoleId, StartOrganizationDomainVerificationResponseContent, UpdateIdentityProviderRequestContent, UpdateIdentityProviderResponseContent, @@ -158,13 +164,12 @@ UnauthorizedError, ) from . import organization, organization_details + from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient from .client import AsyncAuth0, Auth0 from .environment import Auth0Environment from .version import __version__ _dynamic_imports: typing.Dict[str, str] = { "AsyncAuth0": ".client", - "AsyncMyOrganizationClient": ".myorganization_client", - "AsyncTokenProvider": ".token_provider", "Auth0": ".client", "Auth0Environment": ".environment", "Automatic": ".types", @@ -177,10 +182,12 @@ "CreateIdentityProviderResponseContent": ".types", "CreateIdpDomainResponseContent": ".types", "CreateIdpProvisioningScimTokenResponseContent": ".types", + "CreateMemberInvitationInvitee": ".types", "CreateMemberInvitationResponseContent": ".types", "CreateOrganizationDomainResponseContent": ".types", + "DefaultAioHttpClient": "._default_clients", + "DefaultAsyncHttpxClient": "._default_clients", "DomainIdp": ".types", - "DomainVerificationEnum": ".types", "ErrorResponseContent": ".types", "FedMetadataXml": ".types", "ForbiddenError": ".errors", @@ -192,6 +199,7 @@ "GetOrganizationDetailsResponseContent": ".types", "GetOrganizationDomainResponseContent": ".types", "GetOrganizationMemberResponseContent": ".types", + "GetOrganizationMemberRolesResponseContent": ".types", "IdentityProviderConfigAdfs": ".types", "IdentityProviderConfigGoogleApps": ".types", "IdentityProviderConfigOidc": ".types", @@ -200,7 +208,6 @@ "IdentityProviderConfigSamlp": ".types", "IdentityProviderConfigWaad": ".types", "IdentityProvidersConfig": ".types", - "IdentityProvidersConfigDomainAlias": ".types", "IdentityProvidersConfigEnabledFeaturesEnum": ".types", "IdentityProvidersConfigOrganization": ".types", "IdentityProvidersConfigProvisioningMethodsEnum": ".types", @@ -284,17 +291,20 @@ "ListDomainIdentityProvidersResponseContent": ".types", "ListIdentityProvidersResponseContent": ".types", "ListIdpProvisioningScimTokensResponseContent": ".types", + "ListMembersInvitationsResponseContent": ".types", "ListOrganizationDomainsResponseContent": ".types", + "ListOrganizationMembersResponseContent": ".types", + "ListRolesResponseContent": ".types", "Manual": ".types", "MemberInvitation": ".types", "MemberInvitationInvitee": ".types", "MemberInvitationInviter": ".types", - "MyOrganizationClient": ".myorganization_client", "NotFoundError": ".errors", "OauthScope": ".types", "OrgBranding": ".types", "OrgBrandingColors": ".types", "OrgDetails": ".types", + "OrgDetailsRead": ".types", "OrgDomain": ".types", "OrgDomainId": ".types", "OrgDomainName": ".types", @@ -302,11 +312,12 @@ "OrgId": ".types", "OrgMember": ".types", "OrgMemberId": ".types", - "OrgMemberRole": ".types", - "OrgMemberRoleId": ".types", + "OrgMemberIdReadOnly": ".types", "OrganizationAccessLevelEnum": ".types", + "OrganizationMemberRolesChangeRequestContent": ".types", + "Role": ".types", + "RoleId": ".types", "StartOrganizationDomainVerificationResponseContent": ".types", - "TokenProvider": ".token_provider", "TooManyRequestsError": ".errors", "UnauthorizedError": ".errors", "UpdateIdentityProviderRequestContent": ".types", @@ -345,8 +356,6 @@ def __dir__(): __all__ = [ "AsyncAuth0", - "AsyncMyOrganizationClient", - "AsyncTokenProvider", "Auth0", "Auth0Environment", "Automatic", @@ -359,10 +368,12 @@ def __dir__(): "CreateIdentityProviderResponseContent", "CreateIdpDomainResponseContent", "CreateIdpProvisioningScimTokenResponseContent", + "CreateMemberInvitationInvitee", "CreateMemberInvitationResponseContent", "CreateOrganizationDomainResponseContent", + "DefaultAioHttpClient", + "DefaultAsyncHttpxClient", "DomainIdp", - "DomainVerificationEnum", "ErrorResponseContent", "FedMetadataXml", "ForbiddenError", @@ -374,6 +385,7 @@ def __dir__(): "GetOrganizationDetailsResponseContent", "GetOrganizationDomainResponseContent", "GetOrganizationMemberResponseContent", + "GetOrganizationMemberRolesResponseContent", "IdentityProviderConfigAdfs", "IdentityProviderConfigGoogleApps", "IdentityProviderConfigOidc", @@ -382,7 +394,6 @@ def __dir__(): "IdentityProviderConfigSamlp", "IdentityProviderConfigWaad", "IdentityProvidersConfig", - "IdentityProvidersConfigDomainAlias", "IdentityProvidersConfigEnabledFeaturesEnum", "IdentityProvidersConfigOrganization", "IdentityProvidersConfigProvisioningMethodsEnum", @@ -466,17 +477,20 @@ def __dir__(): "ListDomainIdentityProvidersResponseContent", "ListIdentityProvidersResponseContent", "ListIdpProvisioningScimTokensResponseContent", + "ListMembersInvitationsResponseContent", "ListOrganizationDomainsResponseContent", + "ListOrganizationMembersResponseContent", + "ListRolesResponseContent", "Manual", "MemberInvitation", "MemberInvitationInvitee", "MemberInvitationInviter", - "MyOrganizationClient", "NotFoundError", "OauthScope", "OrgBranding", "OrgBrandingColors", "OrgDetails", + "OrgDetailsRead", "OrgDomain", "OrgDomainId", "OrgDomainName", @@ -484,11 +498,12 @@ def __dir__(): "OrgId", "OrgMember", "OrgMemberId", - "OrgMemberRole", - "OrgMemberRoleId", + "OrgMemberIdReadOnly", "OrganizationAccessLevelEnum", + "OrganizationMemberRolesChangeRequestContent", + "Role", + "RoleId", "StartOrganizationDomainVerificationResponseContent", - "TokenProvider", "TooManyRequestsError", "UnauthorizedError", "UpdateIdentityProviderRequestContent", diff --git a/src/auth0/myorganization/client.py b/src/auth0/myorganization/client.py index 815f543..3d124a9 100644 --- a/src/auth0/myorganization/client.py +++ b/src/auth0/myorganization/client.py @@ -42,6 +42,9 @@ class Auth0: timeout : typing.Optional[float] The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + follow_redirects : typing.Optional[bool] Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. @@ -69,6 +72,7 @@ def __init__( token: typing.Union[str, typing.Callable[[], str]], headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, @@ -76,6 +80,7 @@ def __init__( _defaulted_timeout = ( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) + _defaulted_max_retries = max_retries if max_retries is not None else 2 if tenant_domain is not None: _tenant_domain = tenant_domain if tenant_domain is not None else "{TENANT}.auth0.com" base_url = "https://{tenantDomain}/my-org/v1".format(tenantDomain=_tenant_domain) @@ -89,6 +94,7 @@ def __init__( if follow_redirects is not None else httpx.Client(timeout=_defaulted_timeout), timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, logging=logging, ) self._organization_details: typing.Optional[OrganizationDetailsClient] = None @@ -160,6 +166,9 @@ class AsyncAuth0: timeout : typing.Optional[float] The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + follow_redirects : typing.Optional[bool] Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. @@ -188,6 +197,7 @@ def __init__( headers: typing.Optional[typing.Dict[str, str]] = None, async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, @@ -195,6 +205,7 @@ def __init__( _defaulted_timeout = ( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) + _defaulted_max_retries = max_retries if max_retries is not None else 2 if tenant_domain is not None: _tenant_domain = tenant_domain if tenant_domain is not None else "{TENANT}.auth0.com" base_url = "https://{tenantDomain}/my-org/v1".format(tenantDomain=_tenant_domain) @@ -207,6 +218,7 @@ def __init__( if httpx_client is not None else _make_default_async_client(timeout=_defaulted_timeout, follow_redirects=follow_redirects), timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, logging=logging, ) self._organization_details: typing.Optional[AsyncOrganizationDetailsClient] = None diff --git a/src/auth0/myorganization/core/http_client.py b/src/auth0/myorganization/core/http_client.py index f0a39ca..f686c57 100644 --- a/src/auth0/myorganization/core/http_client.py +++ b/src/auth0/myorganization/core/http_client.py @@ -125,8 +125,7 @@ def _retry_timeout_from_retries(retries: int) -> float: def _should_retry(response: httpx.Response) -> bool: - retryable_400s = [429, 408, 409] - return response.status_code >= 500 or response.status_code in retryable_400s + return response.status_code >= 500 or response.status_code in [429, 408, 409] _SENSITIVE_HEADERS = frozenset( diff --git a/src/auth0/myorganization/core/http_sse/_api.py b/src/auth0/myorganization/core/http_sse/_api.py index f900b3b..fd13730 100644 --- a/src/auth0/myorganization/core/http_sse/_api.py +++ b/src/auth0/myorganization/core/http_sse/_api.py @@ -1,14 +1,17 @@ # This file was auto-generated by Fern from our API Definition. +import codecs import re from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast +from typing import Any, AsyncGenerator, AsyncIterator, Iterator import httpx from ._decoders import SSEDecoder from ._exceptions import SSEError from ._models import ServerSentEvent +MAX_LINE_SIZE: int = 1_048_576 # 1 MiB + class EventSource: def __init__(self, response: httpx.Response) -> None: @@ -45,46 +48,101 @@ def _get_charset(self) -> str: def response(self) -> httpx.Response: return self._response + @staticmethod + def _normalize_sse_line_endings(buf: str) -> str: + """Normalize line endings per the SSE spec (\\r\\n → \\n, bare \\r → \\n). + + A trailing \\r is preserved because it may pair with a leading \\n in + the next chunk to form a single \\r\\n terminator. + """ + buf = buf.replace("\r\n", "\n") + if buf.endswith("\r"): + return buf[:-1].replace("\r", "\n") + "\r" + return buf.replace("\r", "\n") + def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() charset = self._get_charset() + text_decoder = codecs.getincrementaldecoder(charset)(errors="replace") - buffer = "" + buf = "" for chunk in self._response.iter_bytes(): - # Decode chunk using detected charset - text_chunk = chunk.decode(charset, errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event if sse is not None: yield sse - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + # Flush any remaining bytes from the incremental decoder + buf += text_decoder.decode(b"", final=True) + buf = buf.replace("\r\n", "\n").replace("\r", "\n") + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + while "\n" in buf: + line, buf = buf.split("\n", 1) sse = decoder.decode(line) if sse is not None: yield sse + if buf.strip(): + sse = decoder.decode(buf) + if sse is not None: + yield sse + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: self._check_content_type() decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") + charset = self._get_charset() + text_decoder = codecs.getincrementaldecoder(charset)(errors="replace") + + buf = "" + async for chunk in self._response.aiter_bytes(): + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) sse = decoder.decode(line) if sse is not None: yield sse - finally: - await lines.aclose() + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + # Flush any remaining bytes from the incremental decoder + buf += text_decoder.decode(b"", final=True) + buf = buf.replace("\r\n", "\n").replace("\r", "\n") + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if buf.strip(): + sse = decoder.decode(buf) + if sse is not None: + yield sse @contextmanager diff --git a/src/auth0/myorganization/core/pydantic_utilities.py b/src/auth0/myorganization/core/pydantic_utilities.py index fea3a08..6587f5e 100644 --- a/src/auth0/myorganization/core/pydantic_utilities.py +++ b/src/auth0/myorganization/core/pydantic_utilities.py @@ -135,111 +135,21 @@ def _decimal_encoder(dec_value: Any) -> Any: Model = TypeVar("Model", bound=pydantic.BaseModel) -def _get_discriminator_and_variants(type_: Type[Any]) -> Tuple[Optional[str], Optional[List[Type[Any]]]]: - """ - Extract the discriminator field name and union variants from a discriminated union type. - Supports Annotated[Union[...], Field(discriminator=...)] patterns. - Returns (discriminator, variants) or (None, None) if not a discriminated union. - """ - origin = typing_extensions.get_origin(type_) - - if origin is typing_extensions.Annotated: - args = typing_extensions.get_args(type_) - if len(args) >= 2: - inner_type = args[0] - # Check annotations for discriminator - discriminator = None - for annotation in args[1:]: - if hasattr(annotation, "discriminator"): - discriminator = getattr(annotation, "discriminator", None) - break - - if discriminator: - inner_origin = typing_extensions.get_origin(inner_type) - if inner_origin is Union: - variants = list(typing_extensions.get_args(inner_type)) - return discriminator, variants - return None, None - - -def _get_field_annotation(model: Type[Any], field_name: str) -> Optional[Type[Any]]: - """Get the type annotation of a field from a Pydantic model.""" - if IS_PYDANTIC_V2: - fields = getattr(model, "model_fields", {}) - field_info = fields.get(field_name) - if field_info: - return cast(Optional[Type[Any]], field_info.annotation) - else: - fields = getattr(model, "__fields__", {}) - field_info = fields.get(field_name) - if field_info: - return cast(Optional[Type[Any]], field_info.outer_type_) - return None - - -def _find_variant_by_discriminator( - variants: List[Type[Any]], - discriminator: str, - discriminator_value: Any, -) -> Optional[Type[Any]]: - """Find the union variant that matches the discriminator value.""" - for variant in variants: - if not (inspect.isclass(variant) and issubclass(variant, pydantic.BaseModel)): - continue - - disc_annotation = _get_field_annotation(variant, discriminator) - if disc_annotation and is_literal_type(disc_annotation): - literal_args = get_args(disc_annotation) - if literal_args and literal_args[0] == discriminator_value: - return variant - return None - - -def _is_string_type(type_: Type[Any]) -> bool: - """Check if a type is str or Optional[str].""" - if type_ is str: - return True - - origin = typing_extensions.get_origin(type_) - if origin is Union: - args = typing_extensions.get_args(type_) - # Optional[str] = Union[str, None] - non_none_args = [a for a in args if a is not type(None)] - if len(non_none_args) == 1 and non_none_args[0] is str: - return True - - return False - - def parse_sse_obj(sse: "ServerSentEvent", type_: Type[T]) -> T: """ Parse a ServerSentEvent into the appropriate type. - Handles two scenarios based on where the discriminator field is located: - - 1. Data-level discrimination: The discriminator (e.g., 'type') is inside the 'data' payload. - The union describes the data content, not the SSE envelope. - -> Returns: json.loads(data) parsed into the type + This function handles data-level discrimination where the discriminator + (e.g., 'type') is inside the 'data' payload. It parses the SSE data field + as JSON and deserializes it into the target type. - Example: ChatStreamResponse with discriminator='type' - Input: ServerSentEvent(event="message", data='{"type": "content-delta", ...}', id="") - Output: ContentDeltaEvent (parsed from data, SSE envelope stripped) - - 2. Event-level discrimination: The discriminator (e.g., 'event') is at the SSE event level. - The union describes the full SSE event structure. - -> Returns: SSE envelope with 'data' field JSON-parsed only if the variant expects non-string - - Example: JobStreamResponse with discriminator='event' - Input: ServerSentEvent(event="ERROR", data='{"code": "FAILED", ...}', id="123") - Output: JobStreamResponse_Error with data as ErrorData object - - But for variants where data is str (like STATUS_UPDATE): - Input: ServerSentEvent(event="STATUS_UPDATE", data='{"status": "processing"}', id="1") - Output: JobStreamResponse_StatusUpdate with data as string (not parsed) + Note: Protocol-level discrimination (where the discriminator comes from + the SSE event: field) is handled at code-generation time and does not + use this function. Args: sse: The ServerSentEvent object to parse - type_: The target discriminated union type + type_: The target type to deserialize into Returns: The parsed object of type T @@ -248,66 +158,30 @@ def parse_sse_obj(sse: "ServerSentEvent", type_: Type[T]) -> T: This function is only available in SDK contexts where http_sse module exists. """ sse_event = asdict(sse) - discriminator, variants = _get_discriminator_and_variants(type_) - - if discriminator is None or variants is None: - # Not a discriminated union - parse the data field as JSON - data_value = sse_event.get("data") - if isinstance(data_value, str) and data_value: - try: - parsed_data = json.loads(data_value) - return parse_obj_as(type_, parsed_data) - except json.JSONDecodeError as e: - _logger.warning( - "Failed to parse SSE data field as JSON: %s, data: %s", - e, - data_value[:100] if len(data_value) > 100 else data_value, - ) - return parse_obj_as(type_, sse_event) - data_value = sse_event.get("data") + if isinstance(data_value, str) and data_value: + try: + parsed_data = json.loads(data_value) + return parse_obj_as(type_, parsed_data) + except json.JSONDecodeError as e: + _logger.warning( + "Failed to parse SSE data field as JSON: %s, data: %s", + e, + data_value[:100] if len(data_value) > 100 else data_value, + ) + return parse_obj_as(type_, sse_event) - # Check if discriminator is at the top level (event-level discrimination) - if discriminator in sse_event: - # Case 2: Event-level discrimination - # Find the matching variant to check if 'data' field needs JSON parsing - disc_value = sse_event.get(discriminator) - matching_variant = _find_variant_by_discriminator(variants, discriminator, disc_value) - - if matching_variant is not None: - # Check what type the variant expects for 'data' - data_type = _get_field_annotation(matching_variant, "data") - if data_type is not None and not _is_string_type(data_type): - # Variant expects non-string data - parse JSON - if isinstance(data_value, str) and data_value: - try: - parsed_data = json.loads(data_value) - new_object = dict(sse_event) - new_object["data"] = parsed_data - return parse_obj_as(type_, new_object) - except json.JSONDecodeError as e: - _logger.warning( - "Failed to parse SSE data field as JSON for event-level discrimination: %s, data: %s", - e, - data_value[:100] if len(data_value) > 100 else data_value, - ) - # Either no matching variant, data is string type, or JSON parse failed - return parse_obj_as(type_, sse_event) - else: - # Case 1: Data-level discrimination - # The discriminator is inside the data payload - extract and parse data only - if isinstance(data_value, str) and data_value: - try: - parsed_data = json.loads(data_value) - return parse_obj_as(type_, parsed_data) - except json.JSONDecodeError as e: - _logger.warning( - "Failed to parse SSE data field as JSON for data-level discrimination: %s, data: %s", - e, - data_value[:100] if len(data_value) > 100 else data_value, - ) - return parse_obj_as(type_, sse_event) +_type_adapter_cache: Dict[int, Any] = {} + + +def _get_type_adapter(type_: Type[Any]) -> Any: + key = id(type_) + adapter = _type_adapter_cache.get(key) + if adapter is None: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + _type_adapter_cache[key] = adapter + return adapter def parse_obj_as(type_: Type[T], object_: Any) -> T: @@ -342,8 +216,8 @@ def parse_obj_as(type_: Type[T], object_: Any) -> T: else: dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") if IS_PYDANTIC_V2: - adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] - return adapter.validate_python(dealiased_object) + adapter = _get_type_adapter(type_) + return adapter.validate_python(dealiased_object) # type: ignore[no-any-return] return pydantic.parse_obj_as(type_, dealiased_object) diff --git a/src/auth0/myorganization/core/serialization.py b/src/auth0/myorganization/core/serialization.py index c36e865..1d753e2 100644 --- a/src/auth0/myorganization/core/serialization.py +++ b/src/auth0/myorganization/core/serialization.py @@ -26,6 +26,75 @@ def __init__(self, *, alias: str) -> None: self.alias = alias +# Resolving type hints (typing.get_type_hints) is expensive because it eval/compiles +# forward-reference annotations. The result is constant for a given type, so we cache it. +# This is critical for hot paths like SSE event parsing, where the same (often large +# discriminated-union) type is converted on every single event. +_type_hints_cache: typing.Dict[typing.Any, typing.Dict[str, typing.Any]] = {} + + +def _get_cached_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + cached = _type_hints_cache.get(expected_type) + except TypeError: + # Unhashable type; resolve without caching. + return _resolve_type_hints(expected_type) + if cached is None: + cached = _resolve_type_hints(expected_type) + _type_hints_cache[expected_type] = cached + return cached + + +def _resolve_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + return typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + # The type contains a circular reference, so we use the __annotations__ attribute directly. + return getattr(expected_type, "__annotations__", {}) + + +# Whether convert_and_respect_annotation_metadata can possibly rewrite anything for a given +# annotation, i.e. whether any reachable model/TypedDict field carries a FieldMetadata alias. +# This is constant per type, so we cache it and use it to short-circuit the recursive walk. +_requires_conversion_cache: typing.Dict[typing.Any, bool] = {} + + +def _requires_conversion(type_: typing.Any) -> bool: + try: + cached = _requires_conversion_cache.get(type_) + except TypeError: + # Unhashable annotation; compute without caching. + return _compute_requires_conversion(type_, set()) + if cached is None: + cached = _compute_requires_conversion(type_, set()) + _requires_conversion_cache[type_] = cached + return cached + + +def _compute_requires_conversion(type_: typing.Any, seen: typing.Set[typing.Any]) -> bool: + clean_type = _remove_annotations(type_) + + try: + if clean_type in seen: + return False + seen = seen | {clean_type} + except TypeError: + # Unhashable type; skip cycle tracking (the type graph is finite in practice). + pass + + # Models / TypedDicts: a field alias here means we must dealias; otherwise recurse into fields. + if (inspect.isclass(clean_type) and issubclass(clean_type, pydantic.BaseModel)) or typing_extensions.is_typeddict( + clean_type + ): + annotations = _get_cached_type_hints(clean_type) + if _get_alias_to_field_name(annotations): + return True + return any(_compute_requires_conversion(hint, seen) for hint in annotations.values()) + + # Containers / unions: recurse into the type arguments (List/Set/Sequence/Dict/Union/etc.). + return any(_compute_requires_conversion(arg, seen) for arg in typing_extensions.get_args(clean_type)) + + def convert_and_respect_annotation_metadata( *, object_: typing.Any, @@ -57,6 +126,13 @@ def convert_and_respect_annotation_metadata( return None if inner_type is None: inner_type = annotation + # The only thing this function ever rewrites is keys that carry a FieldMetadata + # alias. If nothing in the (cached) type graph has such an alias, the conversion is + # a content-identity transform, so we can skip the entire recursive walk. This is + # the hot path for SSE streaming, where a large discriminated union would otherwise + # be traversed on every single event. + if not _requires_conversion(annotation): + return object_ clean_type = _remove_annotations(inner_type) # Pydantic models @@ -160,12 +236,7 @@ def _convert_mapping( direction: typing.Literal["read", "write"], ) -> typing.Mapping[str, object]: converted_object: typing.Dict[str, object] = {} - try: - annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) - except NameError: - # The TypedDict contains a circular reference, so - # we use the __annotations__ attribute directly. - annotations = getattr(expected_type, "__annotations__", {}) + annotations = _get_cached_type_hints(expected_type) aliases_to_field_names = _get_alias_to_field_name(annotations) for key, value in object_.items(): if direction == "read" and key in aliases_to_field_names: @@ -221,12 +292,12 @@ def _remove_annotations(type_: typing.Any) -> typing.Any: def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: - annotations = typing_extensions.get_type_hints(type_, include_extras=True) + annotations = _get_cached_type_hints(type_) return _get_alias_to_field_name(annotations) def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: - annotations = typing_extensions.get_type_hints(type_, include_extras=True) + annotations = _get_cached_type_hints(type_) return _get_field_to_alias_name(annotations) diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py index 1523647..407e6f4 100644 --- a/tests/utils/test_http_client.py +++ b/tests/utils/test_http_client.py @@ -10,6 +10,7 @@ AsyncHttpClient, HttpClient, _build_url, + _should_retry, get_request_body, remove_none_from_dict, ) @@ -660,3 +661,34 @@ async def test_async_base_max_retries_used_as_default(mock_sleep: AsyncMock) -> assert response.status_code == 200 # 1 initial + 3 retries = 4 total attempts assert mock_client.request.call_count == 4 + + +# --------------------------------------------------------------------------- +# _should_retry unit tests +# --------------------------------------------------------------------------- + + +def _make_response(status_code: int) -> httpx.Response: + return httpx.Response(status_code=status_code, content=b"") + + +@pytest.mark.parametrize( + "status_code", + [408, 409, 429, 500, 501, 502, 503, 504, 599], +) +def test_should_retry_retryable_status_codes(status_code: int) -> None: + """Legacy mode: retries on 408, 409, 429, and all >= 500.""" + assert _should_retry(_make_response(status_code)) is True + + +@pytest.mark.parametrize( + "status_code", + [200, 201, 301, 400, 401, 403, 404], +) +def test_should_not_retry_non_retryable_status_codes(status_code: int) -> None: + assert _should_retry(_make_response(status_code)) is False + + +def test_should_retry_599_upper_boundary() -> None: + """Legacy mode retries on >= 500, which includes 599.""" + assert _should_retry(_make_response(599)) is True diff --git a/tests/wire/conftest.py b/tests/wire/conftest.py index 66716d1..c417af0 100644 --- a/tests/wire/conftest.py +++ b/tests/wire/conftest.py @@ -60,7 +60,7 @@ def verify_request_count( test_id: str, method: str, url_path: str, - query_params: Optional[Dict[str, str]], + query_params: Optional[Dict[str, Any]], expected: int, ) -> None: """Verifies the number of requests made to WireMock filtered by test ID for concurrency safety.""" @@ -71,7 +71,12 @@ def verify_request_count( "headers": {"X-Test-Id": {"equalTo": test_id}}, } if query_params: - query_parameters = {k: {"equalTo": v} for k, v in query_params.items()} + query_parameters = {} + for k, v in query_params.items(): + if isinstance(v, list): + query_parameters[k] = {"hasExactly": [{"equalTo": item} for item in v]} + else: + query_parameters[k] = {"equalTo": v} request_body["queryParameters"] = query_parameters response = httpx.post(f"{wiremock_admin_url}/requests/find", json=request_body) assert response.status_code == 200, "Failed to query WireMock requests" diff --git a/tests/wire/test_organization_identityProviders.py b/tests/wire/test_organization_identityProviders.py index 0445eb5..d905140 100644 --- a/tests/wire/test_organization_identityProviders.py +++ b/tests/wire/test_organization_identityProviders.py @@ -17,19 +17,19 @@ def test_organization_identityProviders_create() -> None: client = get_client(test_id) client.organization.identity_providers.create( request=IdpOidcRequest( - name="oidcIdp", strategy="oidc", - domains=["mydomain.com"], - display_name="OIDC IdP", - show_as_button=True, - assign_membership_on_login=False, - is_enabled=True, options=IdpOidcOptionsRequest( type="front_channel", client_id="a8f3b2e7-5d1c-4f9a-8b0d-2e1c3a5b6f7d", client_secret="KzQp2sVxR8nTgMjFhYcEWuLoIbDvUoC6A9B1zX7yWqFjHkGrP5sQdLmNp", discovery_url="https://{yourDomain}/.well-known/openid-configuration", ), + name="oidcIdp", + domains=["mydomain.com"], + display_name="OIDC IdP", + show_as_button=True, + assign_membership_on_login=False, + is_enabled=True, ), ) verify_request_count(test_id, "POST", "/identity-providers", None, 1) diff --git a/wiremock/docker-compose.test.yml b/wiremock/docker-compose.test.yml index 58747d5..95f0ae9 100644 --- a/wiremock/docker-compose.test.yml +++ b/wiremock/docker-compose.test.yml @@ -5,7 +5,7 @@ services: - "0:8080" # Use dynamic port to avoid conflicts with concurrent tests volumes: - ./wiremock-mappings.json:/home/wiremock/mappings/wiremock-mappings.json - command: ["--global-response-templating", "--verbose"] + command: ["--verbose"] healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"] interval: 2s diff --git a/wiremock/wiremock-mappings.json b/wiremock/wiremock-mappings.json index 13ed5f0..e48106c 100644 --- a/wiremock/wiremock-mappings.json +++ b/wiremock/wiremock-mappings.json @@ -1,8 +1,8 @@ { "mappings": [ { - "id": "29a326c1-09e0-4f0b-8b3a-55872064dbed", - "name": "Get Organization details - default", + "id": "7e1a7c64-7b52-4634-91d9-6d6f54b94bff", + "name": "Get Organization details - getOrgDetailsResponseExample", "request": { "urlPathTemplate": "/details", "method": "GET", @@ -19,7 +19,7 @@ "Content-Type": "application/json" } }, - "uuid": "29a326c1-09e0-4f0b-8b3a-55872064dbed", + "uuid": "7e1a7c64-7b52-4634-91d9-6d6f54b94bff", "persistent": true, "priority": 3, "metadata": { @@ -33,8 +33,8 @@ "postServeActions": [] }, { - "id": "175cc45b-1346-42bb-baab-e94c1318da4c", - "name": "Update Organization details - default", + "id": "1ef770f7-cd42-42cc-973f-f17501df9506", + "name": "Update Organization details - updateOrgDetailsResponseExample", "request": { "urlPathTemplate": "/details", "method": "PATCH", @@ -51,7 +51,7 @@ "Content-Type": "application/json" } }, - "uuid": "175cc45b-1346-42bb-baab-e94c1318da4c", + "uuid": "1ef770f7-cd42-42cc-973f-f17501df9506", "persistent": true, "priority": 3, "metadata": { @@ -64,8 +64,8 @@ } }, { - "id": "ddb32f2f-bb17-4551-8c51-a0f78dbb849f", - "name": "Get My Organization API configuration - default", + "id": "6408a1a2-98b8-4c29-b15f-17b2c9b9db75", + "name": "Get My Organization API configuration - getOrgDetailsResponseExample", "request": { "urlPathTemplate": "/config", "method": "GET", @@ -82,7 +82,7 @@ "Content-Type": "application/json" } }, - "uuid": "ddb32f2f-bb17-4551-8c51-a0f78dbb849f", + "uuid": "6408a1a2-98b8-4c29-b15f-17b2c9b9db75", "persistent": true, "priority": 3, "metadata": { @@ -96,8 +96,8 @@ "postServeActions": [] }, { - "id": "bd99fc7f-05db-4c0a-8386-d3665f0719ca", - "name": "List domains - default", + "id": "db02a313-8a43-4071-87d7-0d15bd2d26e0", + "name": "List domains - listOrgDomainsResponseExample", "request": { "urlPathTemplate": "/domains", "method": "GET", @@ -122,7 +122,7 @@ "Content-Type": "application/json" } }, - "uuid": "bd99fc7f-05db-4c0a-8386-d3665f0719ca", + "uuid": "db02a313-8a43-4071-87d7-0d15bd2d26e0", "persistent": true, "priority": 3, "metadata": { @@ -136,8 +136,8 @@ "postServeActions": [] }, { - "id": "0d1dcca2-642a-4199-b3fe-00d5ec1c8bc2", - "name": "Create a domain - default", + "id": "11952e27-c837-49f7-be35-80be3f545a8c", + "name": "Create a domain - createOrgDomainResponseExample", "request": { "urlPathTemplate": "/domains", "method": "POST", @@ -154,7 +154,7 @@ "Content-Type": "application/json" } }, - "uuid": "0d1dcca2-642a-4199-b3fe-00d5ec1c8bc2", + "uuid": "11952e27-c837-49f7-be35-80be3f545a8c", "persistent": true, "priority": 3, "metadata": { @@ -167,8 +167,8 @@ } }, { - "id": "a3c27ba8-01e3-4c19-8725-d3281492d7a6", - "name": "Get a domain - default", + "id": "4444919f-7bcd-4034-aa50-a59f14c9b216", + "name": "Get a domain - getOrgDomainResponseExample", "request": { "urlPathTemplate": "/domains/{domain_id}", "method": "GET", @@ -190,7 +190,7 @@ "Content-Type": "application/json" } }, - "uuid": "a3c27ba8-01e3-4c19-8725-d3281492d7a6", + "uuid": "4444919f-7bcd-4034-aa50-a59f14c9b216", "persistent": true, "priority": 3, "metadata": { @@ -239,8 +239,8 @@ } }, { - "id": "8b89e2ae-9635-46f3-b4e8-fc575b9b6157", - "name": "List Identity Providers - default", + "id": "11d47f7f-d5a5-43f5-aea7-569c6ce3bd63", + "name": "List Identity Providers - listIdPsResponseExample", "request": { "urlPathTemplate": "/identity-providers", "method": "GET", @@ -257,7 +257,7 @@ "Content-Type": "application/json" } }, - "uuid": "8b89e2ae-9635-46f3-b4e8-fc575b9b6157", + "uuid": "11d47f7f-d5a5-43f5-aea7-569c6ce3bd63", "persistent": true, "priority": 3, "metadata": { @@ -271,8 +271,8 @@ "postServeActions": [] }, { - "id": "3e11b717-d0f8-4c19-9f25-9aaba50cba21", - "name": "Create an Identity Provider - default", + "id": "a6f57fc1-f7ac-4b02-8f4d-bd3fe46eecd4", + "name": "Create an Identity Provider - createIdPResponseExample", "request": { "urlPathTemplate": "/identity-providers", "method": "POST", @@ -289,7 +289,7 @@ "Content-Type": "application/json" } }, - "uuid": "3e11b717-d0f8-4c19-9f25-9aaba50cba21", + "uuid": "a6f57fc1-f7ac-4b02-8f4d-bd3fe46eecd4", "persistent": true, "priority": 3, "metadata": { @@ -302,8 +302,8 @@ } }, { - "id": "58ee7dab-8682-4472-892e-bd11d76b0671", - "name": "Get an Identity Provider - default", + "id": "ebff1d9b-9b46-4505-ad12-9dbda71462b0", + "name": "Get an Identity Provider - getIdPResponseExample", "request": { "urlPathTemplate": "/identity-providers/{idp_id}", "method": "GET", @@ -325,7 +325,7 @@ "Content-Type": "application/json" } }, - "uuid": "58ee7dab-8682-4472-892e-bd11d76b0671", + "uuid": "ebff1d9b-9b46-4505-ad12-9dbda71462b0", "persistent": true, "priority": 3, "metadata": { @@ -374,8 +374,8 @@ } }, { - "id": "97299d05-1531-4f32-8e96-5a64fc5751f8", - "name": "Update an Identity Provider - default", + "id": "44ef7b8f-877a-4ec9-8be1-7204c4c0c994", + "name": "Update an Identity Provider - updateIdPResponseExample", "request": { "urlPathTemplate": "/identity-providers/{idp_id}", "method": "PATCH", @@ -397,7 +397,7 @@ "Content-Type": "application/json" } }, - "uuid": "97299d05-1531-4f32-8e96-5a64fc5751f8", + "uuid": "44ef7b8f-877a-4ec9-8be1-7204c4c0c994", "persistent": true, "priority": 3, "metadata": { @@ -410,8 +410,8 @@ } }, { - "id": "9925321b-b5a5-4ccc-abb9-04139e218c65", - "name": "Refresh Identity Provider attribute mapping - default", + "id": "1e4d66ac-950b-4cd7-a16d-65e8adb4a94b", + "name": "Refresh Identity Provider attribute mapping - getIdPResponseExample", "request": { "urlPathTemplate": "/identity-providers/{idp_id}/update-attributes", "method": "PUT", @@ -433,7 +433,7 @@ "Content-Type": "application/json" } }, - "uuid": "9925321b-b5a5-4ccc-abb9-04139e218c65", + "uuid": "1e4d66ac-950b-4cd7-a16d-65e8adb4a94b", "persistent": true, "priority": 3, "metadata": { @@ -482,8 +482,8 @@ } }, { - "id": "8a221b24-aed6-4bfe-bfdb-c4329d8e2181", - "name": "List members - default", + "id": "7d845899-c08f-4508-a7cb-134b5d39faca", + "name": "List members - listOrgDomainsResponseExample", "request": { "urlPathTemplate": "/members", "method": "GET", @@ -514,7 +514,7 @@ "Content-Type": "application/json" } }, - "uuid": "8a221b24-aed6-4bfe-bfdb-c4329d8e2181", + "uuid": "7d845899-c08f-4508-a7cb-134b5d39faca", "persistent": true, "priority": 3, "metadata": { @@ -528,8 +528,8 @@ "postServeActions": [] }, { - "id": "fae3f220-6c07-455b-b799-d153a5d10715", - "name": "Get a member - default", + "id": "9a6aefd1-96f4-48e6-943b-0e62bf9cfd0c", + "name": "Get a member - listOrgDomainsResponseExample", "request": { "urlPathTemplate": "/members/{user_id}", "method": "GET", @@ -559,7 +559,7 @@ "Content-Type": "application/json" } }, - "uuid": "fae3f220-6c07-455b-b799-d153a5d10715", + "uuid": "9a6aefd1-96f4-48e6-943b-0e62bf9cfd0c", "persistent": true, "priority": 3, "metadata": { @@ -572,8 +572,8 @@ } }, { - "id": "615842a1-853a-4670-8ba3-e4825a6bd0af", - "name": "Delete memberships - default", + "id": "c6126f0a-ce12-4ccd-a21c-8313aebc991f", + "name": "Delete memberships - deleteOrganizationMembershipsRequestExample", "request": { "urlPathTemplate": "/delete-memberships", "method": "POST", @@ -590,7 +590,7 @@ "Content-Type": "application/json" } }, - "uuid": "615842a1-853a-4670-8ba3-e4825a6bd0af", + "uuid": "c6126f0a-ce12-4ccd-a21c-8313aebc991f", "persistent": true, "priority": 3, "metadata": { @@ -603,8 +603,8 @@ } }, { - "id": "78503755-437a-4f13-bb75-fbf050b174b1", - "name": "List member invitations - default", + "id": "4e72920b-af60-44e5-a6ba-aede11137221", + "name": "List member invitations - listMembersInvitationsResponseExample", "request": { "urlPathTemplate": "/member-invitations", "method": "GET", @@ -638,7 +638,7 @@ "Content-Type": "application/json" } }, - "uuid": "78503755-437a-4f13-bb75-fbf050b174b1", + "uuid": "4e72920b-af60-44e5-a6ba-aede11137221", "persistent": true, "priority": 3, "metadata": { @@ -652,8 +652,8 @@ "postServeActions": [] }, { - "id": "55c94d5e-e08c-471f-b76f-111cd557f5ca", - "name": "Create member invitations - default", + "id": "81839c8d-109e-443d-835e-bd23c6354a48", + "name": "Create member invitations - listMembersInvitationsResponseExample", "request": { "urlPathTemplate": "/member-invitations", "method": "POST", @@ -670,7 +670,7 @@ "Content-Type": "application/json" } }, - "uuid": "55c94d5e-e08c-471f-b76f-111cd557f5ca", + "uuid": "81839c8d-109e-443d-835e-bd23c6354a48", "persistent": true, "priority": 3, "metadata": { @@ -683,8 +683,8 @@ } }, { - "id": "1a5a7d10-bc8e-44b0-ab8a-e611721f0d64", - "name": "Get a member invitation - default", + "id": "bf390045-ef2a-4a70-8a73-16e2fc5363e0", + "name": "Get a member invitation - GetMemberInvitationResponseExample", "request": { "urlPathTemplate": "/member-invitations/{invitation_id}", "method": "GET", @@ -714,7 +714,7 @@ "Content-Type": "application/json" } }, - "uuid": "1a5a7d10-bc8e-44b0-ab8a-e611721f0d64", + "uuid": "bf390045-ef2a-4a70-8a73-16e2fc5363e0", "persistent": true, "priority": 3, "metadata": { @@ -763,8 +763,8 @@ } }, { - "id": "d7eb85d6-a74e-47c7-b669-d9c33ad0aa81", - "name": "List roles - default", + "id": "7e78a97c-a095-412c-863f-ef31d9d6c7c0", + "name": "List roles - listRolesResponseExample", "request": { "urlPathTemplate": "/roles", "method": "GET", @@ -792,7 +792,7 @@ "Content-Type": "application/json" } }, - "uuid": "d7eb85d6-a74e-47c7-b669-d9c33ad0aa81", + "uuid": "7e78a97c-a095-412c-863f-ef31d9d6c7c0", "persistent": true, "priority": 3, "metadata": { @@ -806,8 +806,8 @@ "postServeActions": [] }, { - "id": "a032561e-549d-47e8-8ebb-dad06d2a6ce4", - "name": "Get Identity Provider configuration - default", + "id": "8b9f07ca-37e5-4810-82fa-fe76436925f8", + "name": "Get Identity Provider configuration - getIdpConfigurationResponseExample", "request": { "urlPathTemplate": "/config/identity-providers", "method": "GET", @@ -824,7 +824,7 @@ "Content-Type": "application/json" } }, - "uuid": "a032561e-549d-47e8-8ebb-dad06d2a6ce4", + "uuid": "8b9f07ca-37e5-4810-82fa-fe76436925f8", "persistent": true, "priority": 3, "metadata": { @@ -838,8 +838,8 @@ "postServeActions": [] }, { - "id": "1a6cb620-9021-415a-98b0-c777a13f658e", - "name": "Start domain verification - default", + "id": "071dc861-6513-45ca-bbde-07ee67629ad7", + "name": "Start domain verification - updateOrgDomainResponseExample", "request": { "urlPathTemplate": "/domains/{domain_id}/verify", "method": "POST", @@ -861,7 +861,7 @@ "Content-Type": "application/json" } }, - "uuid": "1a6cb620-9021-415a-98b0-c777a13f658e", + "uuid": "071dc861-6513-45ca-bbde-07ee67629ad7", "persistent": true, "priority": 3, "metadata": { @@ -874,8 +874,8 @@ } }, { - "id": "736aebae-5a32-4a51-b735-1331ee4bd9ab", - "name": "List Identity Providers for a domain - default", + "id": "883745b4-a5d3-4e55-a102-3f6de84187f1", + "name": "List Identity Providers for a domain - getOrgDomainIdentityProvidersResponseExample", "request": { "urlPathTemplate": "/domains/{domain_id}/identity-providers", "method": "GET", @@ -897,7 +897,7 @@ "Content-Type": "application/json" } }, - "uuid": "736aebae-5a32-4a51-b735-1331ee4bd9ab", + "uuid": "883745b4-a5d3-4e55-a102-3f6de84187f1", "persistent": true, "priority": 3, "metadata": { @@ -910,8 +910,8 @@ } }, { - "id": "79faaaa8-f657-4412-a338-31db2a0c22fc", - "name": "Associate a domain with an Identity Provider - default", + "id": "8d1092ea-dd63-4d47-be98-5400f6d5590b", + "name": "Associate a domain with an Identity Provider - createIdPResponseExample", "request": { "urlPathTemplate": "/identity-providers/{idp_id}/domains", "method": "POST", @@ -933,7 +933,7 @@ "Content-Type": "application/json" } }, - "uuid": "79faaaa8-f657-4412-a338-31db2a0c22fc", + "uuid": "8d1092ea-dd63-4d47-be98-5400f6d5590b", "persistent": true, "priority": 3, "metadata": { @@ -985,8 +985,8 @@ } }, { - "id": "3ef1a37b-4761-4156-9d5f-1330d174d47b", - "name": "Get a Provisioning Configuration - default", + "id": "c24355de-8f84-4694-bd72-97dd531392b5", + "name": "Get a Provisioning Configuration - getIdPResponseExample", "request": { "urlPathTemplate": "/identity-providers/{idp_id}/provisioning", "method": "GET", @@ -1008,7 +1008,7 @@ "Content-Type": "application/json" } }, - "uuid": "3ef1a37b-4761-4156-9d5f-1330d174d47b", + "uuid": "c24355de-8f84-4694-bd72-97dd531392b5", "persistent": true, "priority": 3, "metadata": { @@ -1021,8 +1021,8 @@ } }, { - "id": "a58b006c-012a-4283-b025-5b0fc5d9c644", - "name": "Create a Provisioning Configuration - default", + "id": "028859ea-0007-45a9-9463-044b0d9067e3", + "name": "Create a Provisioning Configuration - createIdPProvisioningResponseExample", "request": { "urlPathTemplate": "/identity-providers/{idp_id}/provisioning", "method": "POST", @@ -1044,7 +1044,7 @@ "Content-Type": "application/json" } }, - "uuid": "a58b006c-012a-4283-b025-5b0fc5d9c644", + "uuid": "028859ea-0007-45a9-9463-044b0d9067e3", "persistent": true, "priority": 3, "metadata": { @@ -1093,8 +1093,8 @@ } }, { - "id": "b52f90fe-1fb8-43af-b6c6-92036c78eafd", - "name": "Refresh Provisioning Configuration attribute mapping - default", + "id": "98cc5ac5-93ef-4cdf-8ef0-9eac5d023886", + "name": "Refresh Provisioning Configuration attribute mapping - getIdPResponseExample", "request": { "urlPathTemplate": "/identity-providers/{idp_id}/provisioning/update-attributes", "method": "PUT", @@ -1116,7 +1116,7 @@ "Content-Type": "application/json" } }, - "uuid": "b52f90fe-1fb8-43af-b6c6-92036c78eafd", + "uuid": "98cc5ac5-93ef-4cdf-8ef0-9eac5d023886", "persistent": true, "priority": 3, "metadata": { @@ -1129,8 +1129,8 @@ } }, { - "id": "e0c33ec4-40b3-42d5-8a5c-0dd959cdb9ce", - "name": "List Provisioning SCIM tokens - default", + "id": "bcdcd71b-d2a7-4d9a-b4ab-22d5e5c95c46", + "name": "List Provisioning SCIM tokens - listOrgDomainsResponseExample", "request": { "urlPathTemplate": "/identity-providers/{idp_id}/provisioning/scim-tokens", "method": "GET", @@ -1152,7 +1152,7 @@ "Content-Type": "application/json" } }, - "uuid": "e0c33ec4-40b3-42d5-8a5c-0dd959cdb9ce", + "uuid": "bcdcd71b-d2a7-4d9a-b4ab-22d5e5c95c46", "persistent": true, "priority": 3, "metadata": { @@ -1165,8 +1165,8 @@ } }, { - "id": "97c46081-373a-430e-a4bb-91e718763cd8", - "name": "Create a Provisioning SCIM token - default", + "id": "1fc598f0-4985-4b9b-89cb-6ab5dc54325a", + "name": "Create a Provisioning SCIM token - createIdPProvisioningScimTokenResponseExample", "request": { "urlPathTemplate": "/identity-providers/{idp_id}/provisioning/scim-tokens", "method": "POST", @@ -1188,7 +1188,7 @@ "Content-Type": "application/json" } }, - "uuid": "97c46081-373a-430e-a4bb-91e718763cd8", + "uuid": "1fc598f0-4985-4b9b-89cb-6ab5dc54325a", "persistent": true, "priority": 3, "metadata": { @@ -1240,8 +1240,8 @@ } }, { - "id": "3b86d015-abf6-4c97-b708-237ed2ad30e3", - "name": "List member roles - default", + "id": "c8ae9a84-d822-47b5-8ec8-a500489495dc", + "name": "List member roles - listOrganizationMemberRolesResponseExample", "request": { "urlPathTemplate": "/members/{user_id}/roles", "method": "GET", @@ -1271,7 +1271,7 @@ "Content-Type": "application/json" } }, - "uuid": "3b86d015-abf6-4c97-b708-237ed2ad30e3", + "uuid": "c8ae9a84-d822-47b5-8ec8-a500489495dc", "persistent": true, "priority": 3, "metadata": { @@ -1284,8 +1284,8 @@ } }, { - "id": "fc74ad7a-df90-438a-80ba-908648d37c76", - "name": "Assign roles to a member - default", + "id": "c3e57548-ed49-4b95-8fe8-a14e5e0c9492", + "name": "Assign roles to a member - assignOrganizationMemberRolesRequestExample", "request": { "urlPathTemplate": "/members/{user_id}/roles", "method": "POST", @@ -1307,7 +1307,7 @@ "Content-Type": "application/json" } }, - "uuid": "fc74ad7a-df90-438a-80ba-908648d37c76", + "uuid": "c3e57548-ed49-4b95-8fe8-a14e5e0c9492", "persistent": true, "priority": 3, "metadata": { @@ -1320,8 +1320,8 @@ } }, { - "id": "da82679e-4aa8-4fe7-b946-2b37ffeb5fdb", - "name": "Remove roles from a member - default", + "id": "c1b9e307-010e-4b6d-8d36-d5053dd3e6fd", + "name": "Remove roles from a member - unassignOrganizationMemberRolesRequestExample", "request": { "urlPathTemplate": "/members/{user_id}/roles", "method": "DELETE", @@ -1343,7 +1343,7 @@ "Content-Type": "application/json" } }, - "uuid": "da82679e-4aa8-4fe7-b946-2b37ffeb5fdb", + "uuid": "c1b9e307-010e-4b6d-8d36-d5053dd3e6fd", "persistent": true, "priority": 3, "metadata": {