|
18 | 18 |
|
19 | 19 | from __future__ import annotations |
20 | 20 |
|
21 | | -import contextlib |
| 21 | +import importlib |
22 | 22 | from collections.abc import Callable |
| 23 | +from typing import TYPE_CHECKING, Any |
23 | 24 |
|
24 | 25 | from vgi_rpc.rpc import AuthContext, CallContext |
25 | 26 |
|
|
43 | 44 | "TokenResolver", |
44 | 45 | ] |
45 | 46 |
|
46 | | -# HTTP auth helpers — available when vgi[http] is installed. |
47 | | -with contextlib.suppress(ImportError): |
48 | | - from vgi_rpc.http import ( # noqa: F401 |
| 47 | +# HTTP auth helpers (``vgi[http]``) and JWT auth (``vgi[oauth]``), resolved on |
| 48 | +# first access rather than at import. ``vgi/__init__`` imports this module, and |
| 49 | +# ``vgi_rpc.http`` brings in the HTTP server and client stack (falcon, httpx2, |
| 50 | +# cryptography, joserfc): importing it here put about 180 ms on every |
| 51 | +# ``import vgi``, and so on the start of every subprocess worker, which never |
| 52 | +# uses it. ``from vgi.auth import bearer_authenticate`` works as before; when |
| 53 | +# the extra is missing it raises ImportError, as it did. |
| 54 | +_LAZY_EXPORTS = { |
| 55 | + "AuthUnavailableError": "vgi_rpc.http", |
| 56 | + "OAuthResourceMetadata": "vgi_rpc.http", |
| 57 | + "bearer_authenticate": "vgi_rpc.http", |
| 58 | + "bearer_authenticate_static": "vgi_rpc.http", |
| 59 | + "chain_authenticate": "vgi_rpc.http", |
| 60 | + "parse_client_id": "vgi_rpc.http", |
| 61 | + "parse_client_secret": "vgi_rpc.http", |
| 62 | + "parse_device_code_client_id": "vgi_rpc.http", |
| 63 | + "parse_device_code_client_secret": "vgi_rpc.http", |
| 64 | + "jwt_authenticate": "vgi_rpc.http._oauth_jwt", |
| 65 | +} |
| 66 | + |
| 67 | +if TYPE_CHECKING: |
| 68 | + from vgi_rpc.http import ( |
49 | 69 | AuthUnavailableError, |
50 | 70 | OAuthResourceMetadata, |
51 | 71 | bearer_authenticate, |
|
56 | 76 | parse_device_code_client_id, |
57 | 77 | parse_device_code_client_secret, |
58 | 78 | ) |
| 79 | + from vgi_rpc.http._oauth_jwt import jwt_authenticate |
| 80 | + |
| 81 | + |
| 82 | +def __getattr__(name: str) -> Any: |
| 83 | + """Import an HTTP or JWT auth helper on first access (PEP 562). |
| 84 | +
|
| 85 | + Args: |
| 86 | + name: The attribute being looked up. |
| 87 | +
|
| 88 | + Returns: |
| 89 | + The helper, which is then cached in this module's namespace. |
| 90 | +
|
| 91 | + Raises: |
| 92 | + AttributeError: If ``name`` is not a helper this module re-exports, or |
| 93 | + its extra is not installed. |
| 94 | + """ |
| 95 | + module_name = _LAZY_EXPORTS.get(name) |
| 96 | + if module_name is None: |
| 97 | + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") |
| 98 | + try: |
| 99 | + value = getattr(importlib.import_module(module_name), name) |
| 100 | + except ImportError as exc: |
| 101 | + raise AttributeError( |
| 102 | + f"module {__name__!r} has no attribute {name!r}: it needs {module_name}, which failed to import ({exc})" |
| 103 | + ) from exc |
| 104 | + globals()[name] = value |
| 105 | + return value |
| 106 | + |
| 107 | + |
| 108 | +def __dir__() -> list[str]: |
| 109 | + """List this module's names, the lazily imported helpers included. |
59 | 110 |
|
60 | | - __all__ += [ |
61 | | - "AuthUnavailableError", |
62 | | - "OAuthResourceMetadata", |
63 | | - "bearer_authenticate", |
64 | | - "bearer_authenticate_static", |
65 | | - "chain_authenticate", |
66 | | - "parse_client_id", |
67 | | - "parse_client_secret", |
68 | | - "parse_device_code_client_id", |
69 | | - "parse_device_code_client_secret", |
70 | | - ] |
71 | | - |
72 | | -# JWT auth — available when vgi[oauth] is installed (requires authlib). |
73 | | -with contextlib.suppress(ImportError): |
74 | | - from vgi_rpc.http._oauth_jwt import jwt_authenticate # noqa: F401 |
75 | | - |
76 | | - __all__ += ["jwt_authenticate"] |
| 111 | + Returns: |
| 112 | + The module's attribute names. |
| 113 | + """ |
| 114 | + return sorted({*globals(), *_LAZY_EXPORTS}) |
0 commit comments