Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,10 @@ docs/_build/
.ipynb_checkpoints

.idea/
.vscode/
.vscode/

# Local environment files (secrets, never commit)
.env
.env.*
!.env.example
!.env.sample
133 changes: 125 additions & 8 deletions generate_frontend_sdk.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,107 @@
import os
import re
import subprocess
import sys
import shutil
import tempfile
from pathlib import Path

# OpenAPI spec URL from Kinde Frontend API
OPENAPI_SPEC_URL = "https://api-spec.kinde.com/kinde-frontend-api-spec.yaml"
OUTPUT_DIR = "kinde_sdk/frontend"
GENERATOR_DIR = "generator"
CONFIG_FILE = f"{GENERATOR_DIR}/frontend_config.yaml"


def _read_sdk_version() -> str:
"""
Read the canonical SDK version from ``kinde_sdk/_version.py``.

The resolved value is supplied to ``openapi-generator-cli`` at runtime
via ``--additional-properties=packageVersion=<SDK_VERSION>`` so that
artifacts which embed the version literally (e.g. user-agent headers
in the generated ``configuration.py``) stay in lockstep with the SDK.
The ``generator/frontend_config.yaml`` file itself holds the literal
placeholder ``"SDK_VERSION"`` (see
``FRONTEND_CONFIG_PACKAGE_VERSION_PLACEHOLDER`` below) so the YAML
can never become a second source of truth.

The generated ``kinde_sdk/frontend/__init__.py`` doesn't keep a literal
copy of the version - it re-exports ``kinde_sdk._version.__version__``,
and ``make_version_dynamic`` (called below after generation) rewrites
the OpenAPI-emitted ``__version__ = "..."`` line back to that import
on every regeneration. This is a belt-and-braces guarantee on top of
the CLI override.
"""
version_path = Path(__file__).resolve().parent / "kinde_sdk" / "_version.py"
text = version_path.read_text(encoding="utf-8")
match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', text, re.MULTILINE)
if not match:
raise RuntimeError(
f"Could not parse __version__ from {version_path}; "
"the generator can't derive packageVersion."
)
return match.group(1)


SDK_VERSION = _read_sdk_version()
Comment thread
KomanRudden marked this conversation as resolved.
Outdated


# Literal placeholder written into ``generator/frontend_config.yaml``'s
# ``packageVersion`` field. The config file is deliberately *not* a version
# literal - it stores this self-documenting placeholder so it cannot be
# mistaken for a second source of truth. The wrapper script always supplies
# the resolved version via ``--additional-properties=packageVersion=<SDK_VERSION>``
# at CLI invocation time, which overrides whatever the file contains. This
# mirrors the management generator's ``openapitools.json`` design; see
# ``generate_management_sdk.py::OPENAPITOOLS_PACKAGE_VERSION_PLACEHOLDER``.
# ``testv2/testv2_core/test_version_sync.py`` asserts the placeholder
# invariant when the file exists in the checkout.
FRONTEND_CONFIG_PACKAGE_VERSION_PLACEHOLDER = "SDK_VERSION"


DYNAMIC_VERSION_IMPORT = (
"from kinde_sdk._version import __version__ "
"# single source of truth; see kinde_sdk/_version.py"
)


def make_version_dynamic(init_file):
"""
Replace the OpenAPI-emitted ``__version__ = "X"`` line in the generated
``__init__.py`` with an import from ``kinde_sdk._version``, so the
sub-package never holds a literal version that can drift.

Idempotent: a no-op if the file already imports from ``kinde_sdk._version``.
"""
init_path = Path(init_file)
if not init_path.exists():
print(f"Warning: cannot rewrite __version__: {init_path} does not exist")
return

text = init_path.read_text(encoding="utf-8")

if "from kinde_sdk._version import __version__" in text:
print(f"{init_path} already imports __version__ from kinde_sdk._version")
return

new_text, n = re.subn(
r'^__version__\s*=\s*["\'][^"\']+["\']\s*$',
DYNAMIC_VERSION_IMPORT,
text,
count=1,
flags=re.MULTILINE,
)
if n == 0:
print(
f"Warning: could not find literal __version__ in {init_path} to replace; "
"is the OpenAPI generator template still emitting one?"
)
return

init_path.write_text(new_text, encoding="utf-8")
print(f"Rewrote __version__ in {init_path} to import from kinde_sdk._version")

def fix_imports(directory):
"""Fix import paths in generated Python files."""
print(f"Fixing imports in {directory}")
Expand Down Expand Up @@ -162,20 +254,37 @@ def restore_custom_files(backup_dir):
os.makedirs(GENERATOR_DIR)
print(f"Created directory: {GENERATOR_DIR}")

# Create frontend config.yaml if it doesn't exist
if not os.path.isfile(CONFIG_FILE):
config_content = """# openapi-generator-cli config for python frontend API
# Always (re)write the frontend config so its ``packageVersion`` field
# stays the self-documenting ``SDK_VERSION`` placeholder. The resolved
# version is supplied at runtime via ``--additional-properties`` on the
# openapi-generator-cli command line (see ``cmd`` below), which overrides
# the placeholder. As an additional safety net, ``make_version_dynamic``
# (called after generation) rewrites the generator-emitted
# ``__version__`` line in the produced ``__init__.py`` to import from
# ``kinde_sdk._version``, so the placeholder never reaches a
# wrapper-generated artifact. This mirrors the management generator's
# treatment of ``openapitools.json`` for end-to-end symmetry.
config_content = f"""# openapi-generator-cli config for python frontend API
# NOTE: this file is regenerated by generate_frontend_sdk.py on every run.
# packageVersion holds the literal placeholder "{FRONTEND_CONFIG_PACKAGE_VERSION_PLACEHOLDER}";
# the resolved version is injected at openapi-generator-cli invocation time
# via --additional-properties. The file is intentionally NOT a source of
# truth for the SDK version - see kinde_sdk/_version.py.

packageName: kinde_sdk.frontend
projectName: kinde-python-sdk
packageVersion: 2.0.0
packageVersion: {FRONTEND_CONFIG_PACKAGE_VERSION_PLACEHOLDER}
# It is recommended to use a released version of the generator.
# For example:
# generatorVersion: "7.13.0"
"""
with open(CONFIG_FILE, 'w') as f:
f.write(config_content)
print(f"Created config file: {CONFIG_FILE}")
with open(CONFIG_FILE, 'w') as f:
f.write(config_content)
print(
f"Wrote config file: {CONFIG_FILE} "
f"(packageVersion={FRONTEND_CONFIG_PACKAGE_VERSION_PLACEHOLDER!r} placeholder; "
f"resolved version {SDK_VERSION} supplied via CLI --additional-properties)"
)

# Create temporary directory for generation
with tempfile.TemporaryDirectory() as temp_dir:
Expand All @@ -187,6 +296,10 @@ def restore_custom_files(backup_dir):
"-g", "python",
"-o", temp_dir,
"-c", CONFIG_FILE,
# Override the YAML's "SDK_VERSION" placeholder with the resolved
# value from kinde_sdk/_version.py. The YAML is intentionally a
# template, not a source of truth.
f"--additional-properties=packageVersion={SDK_VERSION}",
"--skip-validate-spec"
]

Expand Down Expand Up @@ -232,7 +345,11 @@ def restore_custom_files(backup_dir):

# Fix imports in the frontend directory
fix_imports(OUTPUT_DIR)


# Replace the OpenAPI-emitted literal __version__ with an import from
# kinde_sdk._version, so the sub-package never drifts from the SDK.
make_version_dynamic(os.path.join(OUTPUT_DIR, "__init__.py"))

# Preserve custom imports
preserve_custom_imports()

Expand Down
Loading