Skip to content

Autogenerate Async stubs for auth0-python #13826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

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