Skip to content
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ per-file-ignores =
# Y026: Have implicit type aliases
# Y053: have literals >50 characters long
stubs/*_pb2.pyi: Y021, Y023, Y026, Y053
stubs/auth0-python/auth0/_asyncified/**/*.pyi: Y053
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The comment above should be changed from "Generated protobuf files" to just "Generated files".


exclude = .venv*,.git
137 changes: 137 additions & 0 deletions scripts/sync_auth0_python.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import re
import shutil
import sys
from collections.abc import Iterable
from itertools import chain
from pathlib import Path
from subprocess import check_call, run
from textwrap import dedent

ASYNCIFIED_PATH = Path("stubs/auth0-python/auth0/_asyncified")
ASYNCIFY_PYI_PATH = ASYNCIFIED_PATH.parent / "asyncify.pyi"
KEEP_LINES_STARTSWITH = ("from ", "import ", " def ", "class ", "\n")
AUTO_GENERATED_COMMENT = "# AUTOGENERATED BY scripts/sync_auth0_python.py"

BASE_CLASS_RE = re.compile(r"class (\w+?):")
SUBCLASS_RE = re.compile(r"class (\w+?)\((\w+?)\):")
AS_IMPORT_RE = re.compile(r"(.+?) as \1")
IMPORT_TO_ASYNCIFY_RE = re.compile(r"(from \.\w+? import )(\w+?)\n")
METHOD_TO_ASYNCIFY_RE = re.compile(r" def (.+?)\(")


def generate_stubs() -> None:
check_call(("stubgen", "-p", "auth0.authentication", "-p", "auth0.management", "-o", ASYNCIFIED_PATH))

# Move the generated stubs to the right place
shutil.copytree(ASYNCIFIED_PATH / "auth0", ASYNCIFIED_PATH, copy_function=shutil.move, dirs_exist_ok=True)
shutil.rmtree(ASYNCIFIED_PATH / "auth0")

for path_to_remove in (
(ASYNCIFIED_PATH / "authentication" / "__init__.pyi"),
(ASYNCIFIED_PATH / "management" / "__init__.pyi"),
*chain.from_iterable(
[
(already_async, already_async.with_name(already_async.name.removeprefix("async_")))
for already_async in ASYNCIFIED_PATH.rglob("async_*.pyi")
]
),
):
path_to_remove.unlink()


def modify_stubs() -> list[tuple[str, str]]:
base_classes_for_overload: list[tuple[str, str]] = []
subclasses_for_overload: list[tuple[str, str]] = []

# Cleanup and modify the stubs
for stub_path in ASYNCIFIED_PATH.rglob("*.pyi"):
with stub_path.open() as stub_file:
lines = stub_file.readlines()
relative_module = (stub_path.relative_to(ASYNCIFIED_PATH).with_suffix("").as_posix()).replace("/", ".")

# Only keep imports, classes and public non-special methods
stub_content = "".join(
filter(lambda line: "def _" not in line and any(line.startswith(check) for check in KEEP_LINES_STARTSWITH), lines)
)

base_classes_for_overload.extend([(relative_module, match) for match in re.findall(BASE_CLASS_RE, stub_content)])
subclasses_for_overload.extend([(relative_module, groups[0]) for groups in re.findall(SUBCLASS_RE, stub_content)])

# Remove redundant ` as ` imports
stub_content = re.sub(AS_IMPORT_RE, "\\1", stub_content)
# Fix relative imports
stub_content = stub_content.replace("from ..", "from ...")
# Rename same-level local imports to use transformed class names ahead of time
stub_content = re.sub(IMPORT_TO_ASYNCIFY_RE, "\\1_\\2Async\n", stub_content)
# Prep extra imports
stub_content = "from typing import type_check_only\n" + stub_content

# Rename classes to their stub-only asyncified variant and subclass them
# Transform subclasses. These are a bit odd since they may have asyncified methods hidden by base class.
stub_content = re.sub(
SUBCLASS_RE,
dedent(
"""\
@type_check_only
class _\\1Async(_\\2Async):"""
),
stub_content,
)
# Transform base classes
stub_content = re.sub(
BASE_CLASS_RE,
dedent(
f"""\
from auth0.{relative_module} import \\1 # noqa: E402
@type_check_only
class _\\1Async(\\1):"""
),
stub_content,
)
# Update methods to their asyncified variant
stub_content = re.sub(METHOD_TO_ASYNCIFY_RE, " async def \\1_async(", stub_content)
# Fix empty classes
stub_content = stub_content.replace("):\n\n", "): ...\n\n")

stub_path.write_text(f"{AUTO_GENERATED_COMMENT}\n{stub_content}")

# Broader types last
return subclasses_for_overload + base_classes_for_overload


def generate_asyncify_pyi(classes_for_overload: Iterable[tuple[str, str]]) -> None:
imports = ""
overloads = ""
for relative_module, class_name in classes_for_overload:
deduped_class_name = relative_module.replace(".", "_") + class_name
async_class_name = f"_{class_name}Async"
deduped_async_class_name = relative_module.replace(".", "_") + async_class_name
imports += f"from auth0.{relative_module} import {class_name} as {deduped_class_name}\n"
imports += f"from ._asyncified.{relative_module} import {async_class_name} as {deduped_async_class_name}\n"
overloads += f"@overload\ndef asyncify(cls: type[{deduped_class_name}]) -> type[{deduped_async_class_name}]: ...\n"

ASYNCIFY_PYI_PATH.write_text(
f"""\
{AUTO_GENERATED_COMMENT}
from typing import overload, TypeVar

{imports}
_T = TypeVar("_T")

{overloads}
@overload
def asyncify(cls: type[_T]) -> type[_T]: ...
"""
)


def main() -> None:
generate_stubs()
classes_for_overload = modify_stubs()
generate_asyncify_pyi(classes_for_overload)

run((sys.executable, "-m", "pre_commit", "run", "--files", *ASYNCIFIED_PATH.rglob("*.pyi"), ASYNCIFY_PYI_PATH), check=False)


if __name__ == "__main__":
main()
8 changes: 3 additions & 5 deletions stubs/auth0-python/@tests/stubtest_allowlist.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# Omit tests
auth0\.test.*

# Omit _async functions because they aren't present at runtime
# The way these stubs are currently implemented is that we pretend all classes have async methods
# Even though in reality, users need to call `auth0.asyncify.asyncify` to generate async subclasses
auth0\..*_async
# Fake private types that are autogenerated by calling auth0.asyncify.asyncify at runtime
auth0\._asyncified.*

# Inconsistently implemented, ommitted
# Inconsistently implemented, omitted
auth0\.management\.Auth0\..*
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from .base import _AuthenticationBaseAsync

@type_check_only
class _BackChannelLoginAsync(_AuthenticationBaseAsync):
async def back_channel_login_async(self, binding_message: str, login_hint: str, scope: str, **kwargs) -> Any: ...
11 changes: 11 additions & 0 deletions stubs/auth0-python/auth0/_asyncified/authentication/base.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from auth0.authentication.base import AuthenticationBase
from auth0.types import RequestData

@type_check_only
class _AuthenticationBaseAsync(AuthenticationBase):
async def post_async(self, url: str, data: RequestData | None = None, headers: dict[str, str] | None = None) -> Any: ...
async def authenticated_post_async(self, url: str, data: dict[str, Any], headers: dict[str, str] | None = None) -> Any: ...
async def get_async(self, url: str, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None) -> Any: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
23 changes: 23 additions & 0 deletions stubs/auth0-python/auth0/_asyncified/authentication/database.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from .base import _AuthenticationBaseAsync

@type_check_only
class _DatabaseAsync(_AuthenticationBaseAsync):
async def signup_async(
self,
email: str,
password: str,
connection: str,
username: str | None = None,
user_metadata: dict[str, Any] | None = None,
given_name: str | None = None,
family_name: str | None = None,
name: str | None = None,
nickname: str | None = None,
picture: str | None = None,
) -> dict[str, Any]: ...
async def change_password_async(
self, email: str, connection: str, password: str | None = None, organization: str | None = None
) -> str: ...
16 changes: 16 additions & 0 deletions stubs/auth0-python/auth0/_asyncified/authentication/delegated.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from .base import _AuthenticationBaseAsync

@type_check_only
class _DelegatedAsync(_AuthenticationBaseAsync):
async def get_token_async(
self,
target: str,
api_type: str,
grant_type: str,
id_token: str | None = None,
refresh_token: str | None = None,
scope: str = "openid",
) -> Any: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from .base import _AuthenticationBaseAsync

@type_check_only
class _EnterpriseAsync(_AuthenticationBaseAsync):
async def saml_metadata_async(self) -> Any: ...
async def wsfed_metadata_async(self) -> Any: ...
37 changes: 37 additions & 0 deletions stubs/auth0-python/auth0/_asyncified/authentication/get_token.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from .base import _AuthenticationBaseAsync

@type_check_only
class _GetTokenAsync(_AuthenticationBaseAsync):
async def authorization_code_async(
self, code: str, redirect_uri: str | None, grant_type: str = "authorization_code"
) -> Any: ...
async def authorization_code_pkce_async(
self, code_verifier: str, code: str, redirect_uri: str | None, grant_type: str = "authorization_code"
) -> Any: ...
async def client_credentials_async(
self, audience: str, grant_type: str = "client_credentials", organization: str | None = None
) -> Any: ...
async def login_async(
self,
username: str,
password: str,
scope: str | None = None,
realm: str | None = None,
audience: str | None = None,
grant_type: str = "http://auth0.com/oauth/grant-type/password-realm",
forwarded_for: str | None = None,
) -> Any: ...
async def refresh_token_async(self, refresh_token: str, scope: str = "", grant_type: str = "refresh_token") -> Any: ...
async def passwordless_login_async(self, username: str, otp: str, realm: str, scope: str, audience: str) -> Any: ...
async def backchannel_login_async(self, auth_req_id: str, grant_type: str = "urn:openid:params:grant-type:ciba") -> Any: ...
async def access_token_for_connection_async(
self,
subject_token_type: str,
subject_token: str,
requested_token_type: str,
connection: str | None = None,
grant_type: str = "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token",
) -> Any: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from .base import _AuthenticationBaseAsync

@type_check_only
class _PasswordlessAsync(_AuthenticationBaseAsync):
async def email_async(self, email: str, send: str = "link", auth_params: dict[str, str] | None = None) -> Any: ...
async def sms_async(self, phone_number: str) -> Any: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from .base import _AuthenticationBaseAsync

@type_check_only
class _PushedAuthorizationRequestsAsync(_AuthenticationBaseAsync):
async def pushed_authorization_request_async(self, response_type: str, redirect_uri: str, **kwargs) -> Any: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from .base import _AuthenticationBaseAsync

@type_check_only
class _RevokeTokenAsync(_AuthenticationBaseAsync):
async def revoke_refresh_token_async(self, token: str) -> Any: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from .base import _AuthenticationBaseAsync

@type_check_only
class _SocialAsync(_AuthenticationBaseAsync):
async def login_async(self, access_token: str, connection: str, scope: str = "openid") -> Any: ...
8 changes: 8 additions & 0 deletions stubs/auth0-python/auth0/_asyncified/authentication/users.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from auth0.authentication.users import Users

@type_check_only
class _UsersAsync(Users):
async def userinfo_async(self, access_token: str) -> dict[str, Any]: ...
32 changes: 32 additions & 0 deletions stubs/auth0-python/auth0/_asyncified/management/actions.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from auth0.management.actions import Actions

@type_check_only
class _ActionsAsync(Actions):
async def get_actions_async(
self,
trigger_id: str | None = None,
action_name: str | None = None,
deployed: bool | None = None,
installed: bool = False,
page: int | None = None,
per_page: int | None = None,
) -> Any: ...
async def create_action_async(self, body: dict[str, Any]) -> dict[str, Any]: ...
async def update_action_async(self, id: str, body: dict[str, Any]) -> dict[str, Any]: ...
async def get_action_async(self, id: str) -> dict[str, Any]: ...
async def delete_action_async(self, id: str, force: bool = False) -> Any: ...
async def get_triggers_async(self) -> dict[str, Any]: ...
async def get_execution_async(self, id: str) -> dict[str, Any]: ...
async def get_action_versions_async(
self, id: str, page: int | None = None, per_page: int | None = None
) -> dict[str, Any]: ...
async def get_trigger_bindings_async(
self, id: str, page: int | None = None, per_page: int | None = None
) -> dict[str, Any]: ...
async def get_action_version_async(self, action_id: str, version_id: str) -> dict[str, Any]: ...
async def deploy_action_async(self, id: str) -> dict[str, Any]: ...
async def rollback_action_version_async(self, action_id: str, version_id: str) -> dict[str, Any]: ...
async def update_trigger_bindings_async(self, id: str, body: dict[str, Any]) -> dict[str, Any]: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from auth0.management.attack_protection import AttackProtection

@type_check_only
class _AttackProtectionAsync(AttackProtection):
async def get_breached_password_detection_async(self) -> dict[str, Any]: ...
async def update_breached_password_detection_async(self, body: dict[str, Any]) -> dict[str, Any]: ...
async def get_brute_force_protection_async(self) -> dict[str, Any]: ...
async def update_brute_force_protection_async(self, body: dict[str, Any]) -> dict[str, Any]: ...
async def get_suspicious_ip_throttling_async(self) -> dict[str, Any]: ...
async def update_suspicious_ip_throttling_async(self, body: dict[str, Any]) -> dict[str, Any]: ...
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import type_check_only

from auth0.management.blacklists import Blacklists

@type_check_only
class _BlacklistsAsync(Blacklists):
async def get_async(self, aud: str | None = None) -> list[dict[str, str]]: ...
async def create_async(self, jti: str, aud: str | None = None) -> dict[str, str]: ...
17 changes: 17 additions & 0 deletions stubs/auth0-python/auth0/_asyncified/management/branding.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# AUTOGENERATED BY scripts/sync_auth0_python.py
from typing import Any, type_check_only

from auth0.management.branding import Branding

@type_check_only
class _BrandingAsync(Branding):
async def get_async(self) -> dict[str, Any]: ...
async def update_async(self, body: dict[str, Any]) -> dict[str, Any]: ...
async def get_template_universal_login_async(self) -> dict[str, Any]: ...
async def delete_template_universal_login_async(self) -> Any: ...
async def update_template_universal_login_async(self, body: dict[str, Any]) -> dict[str, Any]: ...
async def get_default_branding_theme_async(self) -> dict[str, Any]: ...
async def get_branding_theme_async(self, theme_id: str) -> dict[str, Any]: ...
async def delete_branding_theme_async(self, theme_id: str) -> Any: ...
async def update_branding_theme_async(self, theme_id: str, body: dict[str, Any]) -> dict[str, Any]: ...
async def create_branding_theme_async(self, body: dict[str, Any]) -> dict[str, Any]: ...
Loading