Skip to content

Commit 02da024

Browse files
committed
Add missing _sdk_generator_utils.py to complete the shared-helpers refactor
1 parent dd3e1e5 commit 02da024

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

_sdk_generator_utils.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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

Comments
 (0)