|
| 1 | +"""Shared helpers for the OpenAPI generator wrapper scripts. |
| 2 | +
|
| 3 | +Both ``generate_management_sdk.py`` and ``generate_frontend_sdk.py`` need |
| 4 | +to do the same handful of version-related operations: resolve the SDK |
| 5 | +version from ``kinde_sdk/_version.py``, know the |
| 6 | +``"SDK_VERSION"`` placeholder used in the generator config files, and |
| 7 | +post-process the OpenAPI-emitted ``__init__.py`` so its ``__version__`` |
| 8 | +is imported from ``kinde_sdk._version`` rather than baked in as a |
| 9 | +literal. Keeping a single implementation of each here eliminates the |
| 10 | +slow drift that was starting to appear between the two scripts (e.g. |
| 11 | +diverging type annotations, log message styling, and error handling). |
| 12 | +
|
| 13 | +The module is intentionally named with a leading underscore to signal |
| 14 | +that it is internal to this repository's generator tooling; it is not |
| 15 | +part of the public ``kinde_sdk`` package and is not shipped as part of |
| 16 | +the distribution. |
| 17 | +""" |
| 18 | + |
| 19 | +from __future__ import annotations |
| 20 | + |
| 21 | +import re |
| 22 | +from pathlib import Path |
| 23 | + |
| 24 | + |
| 25 | +# --------------------------------------------------------------------------- |
| 26 | +# Version resolution |
| 27 | +# --------------------------------------------------------------------------- |
| 28 | + |
| 29 | +# Path to the single source of truth for the SDK version. Both generator |
| 30 | +# scripts sit at the repo root next to ``kinde_sdk/``, and so does this |
| 31 | +# module - so ``Path(__file__).resolve().parent`` is the repo root. |
| 32 | +_VERSION_FILE = Path(__file__).resolve().parent / "kinde_sdk" / "_version.py" |
| 33 | + |
| 34 | + |
| 35 | +def read_sdk_version() -> str: |
| 36 | + """Read the canonical SDK version string from ``kinde_sdk/_version.py``. |
| 37 | +
|
| 38 | + The resolved value is supplied to ``openapi-generator-cli`` at |
| 39 | + runtime via ``--additional-properties=packageVersion=<SDK_VERSION>`` |
| 40 | + so that artifacts which embed the version literally (e.g. user-agent |
| 41 | + headers in the generated ``configuration.py``) stay in lockstep with |
| 42 | + the SDK. The generator config files themselves |
| 43 | + (``openapitools.json`` and ``generator/frontend_config.yaml``) |
| 44 | + hold the literal placeholder :data:`PACKAGE_VERSION_PLACEHOLDER` so |
| 45 | + they cannot become second sources of truth; see |
| 46 | + ``testv2/testv2_core/test_version_sync.py`` for the enforcing tests. |
| 47 | +
|
| 48 | + Raises: |
| 49 | + RuntimeError: if ``_version.py`` exists but no ``__version__`` |
| 50 | + assignment can be parsed out of it. |
| 51 | + """ |
| 52 | + text = _VERSION_FILE.read_text(encoding="utf-8") |
| 53 | + match = re.search( |
| 54 | + r'^__version__\s*=\s*["\']([^"\']+)["\']', text, re.MULTILINE |
| 55 | + ) |
| 56 | + if not match: |
| 57 | + raise RuntimeError( |
| 58 | + f"Could not parse __version__ from {_VERSION_FILE}; " |
| 59 | + "the generator can't derive packageVersion." |
| 60 | + ) |
| 61 | + return match.group(1) |
| 62 | + |
| 63 | + |
| 64 | +#: Resolved SDK version, ready for both generator scripts to use. |
| 65 | +SDK_VERSION: str = read_sdk_version() |
| 66 | + |
| 67 | + |
| 68 | +# --------------------------------------------------------------------------- |
| 69 | +# Generator-config placeholder |
| 70 | +# --------------------------------------------------------------------------- |
| 71 | + |
| 72 | +#: Literal string written into the ``packageVersion`` field of both |
| 73 | +#: generator config files (``openapitools.json`` and |
| 74 | +#: ``generator/frontend_config.yaml``). The committed/written value is |
| 75 | +#: deliberately *not* a version literal - it is this self-documenting |
| 76 | +#: placeholder, so the files cannot be mistaken for a second source of |
| 77 | +#: truth. The resolved version is injected at runtime via |
| 78 | +#: ``--additional-properties=packageVersion=<SDK_VERSION>`` on the |
| 79 | +#: ``openapi-generator-cli`` command line, which overrides whatever the |
| 80 | +#: file contains. The placeholder invariant is asserted in CI by |
| 81 | +#: ``testv2/testv2_core/test_version_sync.py``. |
| 82 | +PACKAGE_VERSION_PLACEHOLDER: str = "SDK_VERSION" |
| 83 | + |
| 84 | + |
| 85 | +# --------------------------------------------------------------------------- |
| 86 | +# Post-generation ``__version__`` rewrite |
| 87 | +# --------------------------------------------------------------------------- |
| 88 | + |
| 89 | +#: Line written into the generated sub-package ``__init__.py`` in place |
| 90 | +#: of the OpenAPI-emitted ``__version__ = "..."`` literal. Holding a |
| 91 | +#: single constant here means both generator scripts (and any future |
| 92 | +#: ones) emit byte-identical replacements. |
| 93 | +DYNAMIC_VERSION_IMPORT: str = ( |
| 94 | + "from kinde_sdk._version import __version__ " |
| 95 | + "# single source of truth; see kinde_sdk/_version.py" |
| 96 | +) |
| 97 | + |
| 98 | + |
| 99 | +_LITERAL_VERSION_LINE = re.compile( |
| 100 | + r'^__version__\s*=\s*["\'][^"\']+["\']\s*$', |
| 101 | + re.MULTILINE, |
| 102 | +) |
| 103 | + |
| 104 | + |
| 105 | +def make_version_dynamic(init_file: Path) -> None: |
| 106 | + """Rewrite the ``__version__`` literal in a generated ``__init__.py``. |
| 107 | +
|
| 108 | + Replaces the OpenAPI-emitted ``__version__ = "X"`` line with |
| 109 | + :data:`DYNAMIC_VERSION_IMPORT` so the sub-package re-exports the |
| 110 | + canonical value from ``kinde_sdk._version`` and can never drift from |
| 111 | + the SDK source of truth. |
| 112 | +
|
| 113 | + Idempotent: a no-op if the file already imports ``__version__`` from |
| 114 | + ``kinde_sdk._version``. Tolerant of missing files and missing |
| 115 | + ``__version__`` lines - logs a warning and returns rather than |
| 116 | + raising, because the calling scripts treat this as a best-effort |
| 117 | + post-generation step. |
| 118 | +
|
| 119 | + Args: |
| 120 | + init_file: Path to the generated sub-package ``__init__.py``. |
| 121 | + Always passed as a :class:`pathlib.Path` (callers convert |
| 122 | + string paths via :func:`pathlib.Path` at the call site). |
| 123 | + """ |
| 124 | + if not init_file.exists(): |
| 125 | + print(f"⚠️ Cannot rewrite __version__: {init_file} does not exist") |
| 126 | + return |
| 127 | + |
| 128 | + text = init_file.read_text(encoding="utf-8") |
| 129 | + |
| 130 | + if "from kinde_sdk._version import __version__" in text: |
| 131 | + print(f"✓ {init_file} already imports __version__ from kinde_sdk._version") |
| 132 | + return |
| 133 | + |
| 134 | + new_text, n = _LITERAL_VERSION_LINE.subn( |
| 135 | + DYNAMIC_VERSION_IMPORT, text, count=1 |
| 136 | + ) |
| 137 | + if n == 0: |
| 138 | + print( |
| 139 | + f"⚠️ Could not find a literal __version__ in {init_file} to replace; " |
| 140 | + "is the OpenAPI generator template still emitting one?" |
| 141 | + ) |
| 142 | + return |
| 143 | + |
| 144 | + init_file.write_text(new_text, encoding="utf-8") |
| 145 | + print(f"✓ Rewrote __version__ in {init_file} to import from kinde_sdk._version") |
| 146 | + |
| 147 | + |
| 148 | +__all__ = [ |
| 149 | + "DYNAMIC_VERSION_IMPORT", |
| 150 | + "PACKAGE_VERSION_PLACEHOLDER", |
| 151 | + "SDK_VERSION", |
| 152 | + "make_version_dynamic", |
| 153 | + "read_sdk_version", |
| 154 | +] |
0 commit comments