Skip to content
Open
17 changes: 16 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ var/
.installed.cfg
*.egg

# Poetry is not the supported install path for this project (build-backend
# is setuptools; CI uses `pip install -r requirements.txt`). A committed
# poetry.lock creates a two-tier dependency-resolution story - pip users
# get version ranges from pyproject.toml/requirements.txt while Poetry
# users get pinned versions from the lock file - which can drift. If
# Poetry adoption is ever formalised, that migration belongs in a
# dedicated PR (with [tool.poetry] sections and CI updates).
poetry.lock

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
Expand Down Expand Up @@ -61,4 +70,10 @@ docs/_build/
.ipynb_checkpoints

.idea/
.vscode/
.vscode/

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

from _sdk_generator_utils import (
PACKAGE_VERSION_PLACEHOLDER,
SDK_VERSION,
make_version_dynamic as _make_version_dynamic,
)

# 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"


# Re-export under the domain-specific name used in this script's log
# messages, docstrings, and the comments embedded in
# ``generator/frontend_config.yaml``. This is just an alias for
# ``_sdk_generator_utils.PACKAGE_VERSION_PLACEHOLDER`` - the shared
# constant remains the single source of truth, so the two generator
# scripts cannot drift on the placeholder value.
FRONTEND_CONFIG_PACKAGE_VERSION_PLACEHOLDER = PACKAGE_VERSION_PLACEHOLDER


def make_version_dynamic(init_file) -> None:
"""Thin wrapper that accepts ``str`` paths and forwards to the shared util.

The shared implementation in ``_sdk_generator_utils`` is typed
``init_file: Path``, but this script historically passed
``os.path.join(...)`` strings; converting at this single call-site
keeps the public signature stable while still routing all rewrite
logic through one canonical implementation.
"""
_make_version_dynamic(Path(init_file))

def fix_imports(directory):
"""Fix import paths in generated Python files."""
print(f"Fixing imports in {directory}")
Expand Down Expand Up @@ -162,20 +191,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 +233,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 +282,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
87 changes: 76 additions & 11 deletions generate_management_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
from pathlib import Path
from typing import Dict, Any

from _sdk_generator_utils import (
DYNAMIC_VERSION_IMPORT,
PACKAGE_VERSION_PLACEHOLDER,
SDK_VERSION,
make_version_dynamic,
)

# Configure logging
logging.basicConfig(
level=logging.INFO,
Expand All @@ -32,6 +39,14 @@
logger = logging.getLogger(__name__)


# Re-export under the domain-specific name used in this script's log
# messages, docstrings, and the comments inside ``openapitools.json``.
# This is just an alias for ``_sdk_generator_utils.PACKAGE_VERSION_PLACEHOLDER``
# - the shared constant remains the single source of truth, so the two
# generator scripts cannot drift on the placeholder value.
OPENAPITOOLS_PACKAGE_VERSION_PLACEHOLDER = PACKAGE_VERSION_PLACEHOLDER


# =============================================================================
# SDK CONFIGURATION
# =============================================================================
Expand Down Expand Up @@ -297,8 +312,33 @@ def ensure_openapi_generator_ignore(config: Dict[str, Any]):

def ensure_openapitools_config(config: Dict[str, Any]):
"""
Ensure openapitools.json has the correct configuration for this SDK.

Authoritatively (re)write ``openapitools.json`` so the committed file is
always self-evidently a *template*, never a second source of truth for
the SDK version.

Design: the ``packageVersion`` field in the file is set to the literal
placeholder ``"SDK_VERSION"`` (the same name as the Python constant in
this script that holds the resolved value). Anyone reading the file
sees immediately that this is a placeholder, not a version literal.

The resolved version is supplied at runtime via
``--additional-properties=packageVersion=<SDK_VERSION>`` on the
``openapi-generator-cli`` command line (see ``generate_sdk``), which
overrides the file's placeholder. As an additional safety net,
``make_version_dynamic`` 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.

Direct ``openapi-generator-cli`` invocations that bypass the wrapper
(and therefore don't pass ``--additional-properties``) will produce a
``configuration.py`` whose user-agent reads ``"OpenAPI-Generator/SDK_VERSION/python"``.
That is intentionally obvious-broken: it loudly signals "use the
wrapper script" rather than silently shipping a stale version.

``testv2/testv2_core/test_version_sync.py`` asserts this placeholder
invariant in CI so the file cannot regress to a literal version.

Args:
config: SDK configuration dictionary
"""
Expand All @@ -307,7 +347,6 @@ def ensure_openapitools_config(config: Dict[str, Any]):
config_file = Path(config["openapi_tools_config"])
generator_name = config["openapi_tools_generator_name"]

# Define the expected configuration
expected_generator_config = {
"generatorName": "python",
"inputSpec": config["spec_url"],
Expand All @@ -316,18 +355,28 @@ def ensure_openapitools_config(config: Dict[str, Any]):
"additionalProperties": {
"packageName": config["package_name"],
"projectName": "kinde-python-sdk",
"packageVersion": "2.0.0",
# Self-documenting placeholder; resolved version is supplied at
# runtime via --additional-properties (see generate_sdk).
"packageVersion": OPENAPITOOLS_PACKAGE_VERSION_PLACEHOLDER,
"library": "urllib3",
"generateSourceCodeOnly": True # CRITICAL: Only generate source code, not project templates
},
"skipValidateSpec": True,
"files": {}
}

# Read existing config or create new one
previous_package_version = None
if config_file.exists():
with open(config_file, 'r', encoding='utf-8') as f:
openapitools_config = json.load(f)
previous_package_version = (
openapitools_config
.get("generator-cli", {})
.get("generators", {})
.get(generator_name, {})
.get("additionalProperties", {})
.get("packageVersion")
)
else:
openapitools_config = {
"$schema": "https://raw.githubusercontent.com/OpenAPITools/openapi-generator-cli/master/apps/generator-cli/src/config.schema.json",
Expand All @@ -339,7 +388,6 @@ def ensure_openapitools_config(config: Dict[str, Any]):
}
}

# Ensure generators section exists
if "generator-cli" not in openapitools_config:
openapitools_config["generator-cli"] = {
"version": "7.9.0",
Expand All @@ -350,14 +398,27 @@ def ensure_openapitools_config(config: Dict[str, Any]):
if "generators" not in openapitools_config["generator-cli"]:
openapitools_config["generator-cli"]["generators"] = {}

# Update or add the generator configuration
openapitools_config["generator-cli"]["generators"][generator_name] = expected_generator_config

# Write back to file
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(openapitools_config, f, indent=2)

print(f"✓ Updated {config_file} for {generator_name}")
if (
previous_package_version
and previous_package_version != OPENAPITOOLS_PACKAGE_VERSION_PLACEHOLDER
):
print(
f"✓ Reset {config_file} packageVersion from {previous_package_version!r} "
f"back to the {OPENAPITOOLS_PACKAGE_VERSION_PLACEHOLDER!r} placeholder. "
f"The resolved version ({SDK_VERSION}) is injected via "
f"--additional-properties at openapi-generator-cli invocation time."
)
else:
print(
f"✓ Confirmed {config_file} packageVersion is the "
f"{OPENAPITOOLS_PACKAGE_VERSION_PLACEHOLDER!r} placeholder "
f"(resolved version {SDK_VERSION} supplied via CLI --additional-properties)"
)


# =============================================================================
Expand Down Expand Up @@ -388,7 +449,7 @@ def generate_sdk(config: Dict[str, Any]) -> bool:
additional_props = ",".join([
f"packageName={config['package_name']}",
"projectName=kinde-python-sdk",
"packageVersion=2.0.0",
f"packageVersion={SDK_VERSION}",
"library=urllib3",
"generateSourceCodeOnly=true"
])
Expand Down Expand Up @@ -731,7 +792,11 @@ def generate_single_sdk(skip_tests: bool, no_diff: bool) -> bool:

# Step 2: Fix imports
fix_imports(config)


# Step 2b: Replace the OpenAPI-emitted literal __version__ with an import
# from kinde_sdk._version, so this sub-package never drifts from the SDK.
make_version_dynamic(Path(config["output_dir"]) / "__init__.py")

# Step 3: Add custom imports
add_custom_imports(config)

Expand Down
2 changes: 1 addition & 1 deletion kinde_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from kinde_sdk.core.framework.null_framework import NullFramework
from kinde_sdk.core.session_management import KindeSessionManagement

__version__ = "2.2.0"
from kinde_sdk._version import __version__ # noqa: F401 (re-exported)

__all__ = [
"OAuth",
Expand Down
36 changes: 36 additions & 0 deletions kinde_sdk/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Single source of truth for the Kinde Python SDK version.

This module is intentionally minimal: it must not import anything else,
so it can be safely imported from any sub-package's ``__init__`` without
risking a circular import with ``kinde_sdk/__init__.py``.

Derived surfaces (all kept in sync from this string):

* ``kinde_sdk.__version__`` re-exports it directly.
* The generated ``kinde_sdk/management/__init__.py`` and
``kinde_sdk/frontend/__init__.py`` re-export it via a post-generation
rewrite, so all three namespaces report the same version.
* ``pyproject.toml`` resolves its dynamic version from this attribute,
so distribution metadata (and the ``SDKTracker`` User-Agent) matches.
* Both OpenAPI generator configs - ``openapitools.json`` (management,
tracked in git) and ``generator/frontend_config.yaml`` (frontend,
transient and rewritten on each generator run) - intentionally store
the literal placeholder ``"SDK_VERSION"`` in their ``packageVersion``
field rather than a version literal, so neither file can be mistaken
for a second source of truth. The resolved value is injected at
runtime by the respective wrapper script via
``--additional-properties=packageVersion=<SDK_VERSION>`` on the
``openapi-generator-cli`` command line, and the post-generation
``make_version_dynamic`` rewrite imports ``__version__`` from this
module so the placeholder never reaches a wrapper-generated artifact.
``testv2/testv2_core/test_version_sync.py`` enforces both placeholder
invariants in CI.

Bump this string on every release; nothing else needs touching by hand.
After bumping, run both generator scripts (or rely on a release pipeline
that does) so any regenerated sub-package files are committed alongside.
The generator-config files themselves are unaffected by the bump - they
permanently hold the ``"SDK_VERSION"`` placeholder.
"""

__version__ = "2.3.0"
2 changes: 1 addition & 1 deletion kinde_sdk/frontend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
""" # noqa: E501


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

# import apis into sdk package
from kinde_sdk.frontend.api.billing_api import BillingApi
Expand Down
2 changes: 1 addition & 1 deletion kinde_sdk/management/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
""" # noqa: E501


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

# Define package exports
__all__ = [
Expand Down
2 changes: 1 addition & 1 deletion openapitools.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"additionalProperties": {
"packageName": "kinde_sdk.management",
"projectName": "kinde-python-sdk",
"packageVersion": "2.0.0",
"packageVersion": "SDK_VERSION",
"library": "urllib3",
"generateSourceCodeOnly": true
},
Expand Down
Loading