mirror of
https://github.com/apache/superset.git
synced 2026-04-18 15:44:57 +00:00
fix(extensions): enforce correct naming conventions (#38167)
This commit is contained in:
@@ -38,7 +38,20 @@ from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from superset_extensions_cli.constants import MIN_NPM_VERSION
|
||||
from superset_extensions_cli.utils import read_json, read_toml
|
||||
from superset_extensions_cli.exceptions import ExtensionNameError
|
||||
from superset_extensions_cli.types import ExtensionNames
|
||||
from superset_extensions_cli.utils import (
|
||||
generate_extension_names,
|
||||
kebab_to_camel_case,
|
||||
kebab_to_snake_case,
|
||||
read_json,
|
||||
read_toml,
|
||||
to_kebab_case,
|
||||
to_snake_case,
|
||||
validate_extension_id,
|
||||
validate_npm_package_name,
|
||||
validate_python_package_name,
|
||||
)
|
||||
|
||||
REMOTE_ENTRY_REGEX = re.compile(r"^remoteEntry\..+\.js$")
|
||||
FRONTEND_DIST_REGEX = re.compile(r"/frontend/dist")
|
||||
@@ -403,14 +416,127 @@ def dev(ctx: click.Context) -> None:
|
||||
click.secho("❌ No directories to watch. Exiting.", fg="red")
|
||||
|
||||
|
||||
def prompt_for_extension_name(
|
||||
display_name_opt: str | None, id_opt: str | None
|
||||
) -> ExtensionNames:
|
||||
"""
|
||||
Prompt for extension name with graceful validation and re-prompting.
|
||||
|
||||
Args:
|
||||
display_name_opt: Display name provided via CLI option (if any)
|
||||
id_opt: Extension ID provided via CLI option (if any)
|
||||
|
||||
Returns:
|
||||
ExtensionNames: Validated extension name variants
|
||||
"""
|
||||
|
||||
# Case 1: Both provided via CLI - validate they work together
|
||||
if display_name_opt and id_opt:
|
||||
try:
|
||||
# Generate all names from display name for consistency
|
||||
temp_names = generate_extension_names(display_name_opt)
|
||||
# Check if the provided ID matches what we'd generate
|
||||
if temp_names["id"] == id_opt:
|
||||
return temp_names
|
||||
else:
|
||||
# If IDs don't match, use the provided ID but validate it
|
||||
validate_extension_id(id_opt)
|
||||
validate_python_package_name(to_snake_case(id_opt))
|
||||
validate_npm_package_name(id_opt)
|
||||
# Create names with the provided ID (derive technical names from ID)
|
||||
return ExtensionNames(
|
||||
name=display_name_opt,
|
||||
id=id_opt,
|
||||
mf_name=kebab_to_camel_case(id_opt),
|
||||
backend_name=kebab_to_snake_case(id_opt),
|
||||
backend_package=f"superset_extensions.{kebab_to_snake_case(id_opt)}",
|
||||
backend_entry=f"superset_extensions.{kebab_to_snake_case(id_opt)}.entrypoint",
|
||||
)
|
||||
except ExtensionNameError as e:
|
||||
click.secho(f"❌ {e}", fg="red")
|
||||
sys.exit(1)
|
||||
|
||||
# Case 2: Only display name provided - suggest ID
|
||||
if display_name_opt and not id_opt:
|
||||
display_name = display_name_opt
|
||||
try:
|
||||
suggested_names = generate_extension_names(display_name)
|
||||
suggested_id = suggested_names["id"]
|
||||
except ExtensionNameError:
|
||||
suggested_id = to_kebab_case(display_name)
|
||||
|
||||
extension_id = click.prompt("Extension ID", default=suggested_id, type=str)
|
||||
|
||||
# Case 3: Only ID provided - ask for display name
|
||||
elif id_opt and not display_name_opt:
|
||||
extension_id = id_opt
|
||||
# Validate the provided ID first
|
||||
try:
|
||||
validate_extension_id(id_opt)
|
||||
except ExtensionNameError as e:
|
||||
click.secho(f"❌ {e}", fg="red")
|
||||
sys.exit(1)
|
||||
|
||||
# Suggest display name from kebab ID
|
||||
suggested_display = " ".join(word.capitalize() for word in id_opt.split("-"))
|
||||
display_name = click.prompt(
|
||||
"Extension name", default=suggested_display, type=str
|
||||
)
|
||||
|
||||
# Case 4: Neither provided - ask for both
|
||||
else:
|
||||
display_name = click.prompt("Extension name (e.g. Hello World)", type=str)
|
||||
try:
|
||||
suggested_names = generate_extension_names(display_name)
|
||||
suggested_id = suggested_names["id"]
|
||||
except ExtensionNameError:
|
||||
suggested_id = to_kebab_case(display_name)
|
||||
|
||||
extension_id = click.prompt("Extension ID", default=suggested_id, type=str)
|
||||
|
||||
# Final validation loop - try to use generate_extension_names for consistent results
|
||||
display_name_failed = False # Track if display name validation failed
|
||||
while True:
|
||||
try:
|
||||
# First try to generate from display name if possible and it hasn't failed before
|
||||
if display_name and not display_name_failed:
|
||||
temp_names = generate_extension_names(display_name)
|
||||
if temp_names["id"] == extension_id:
|
||||
# Perfect match - use generated names
|
||||
return temp_names
|
||||
|
||||
# If no match or display name failed, validate manually and construct
|
||||
validate_extension_id(extension_id)
|
||||
validate_python_package_name(to_snake_case(extension_id))
|
||||
validate_npm_package_name(extension_id)
|
||||
|
||||
return ExtensionNames(
|
||||
name=display_name,
|
||||
id=extension_id,
|
||||
mf_name=kebab_to_camel_case(extension_id),
|
||||
backend_name=kebab_to_snake_case(extension_id),
|
||||
backend_package=f"superset_extensions.{kebab_to_snake_case(extension_id)}",
|
||||
backend_entry=f"superset_extensions.{kebab_to_snake_case(extension_id)}.entrypoint",
|
||||
)
|
||||
|
||||
except ExtensionNameError as e:
|
||||
click.secho(f"❌ {e}", fg="red")
|
||||
# If the error came from generate_extension_names, stop trying it
|
||||
if "display_name" in str(e) or not display_name_failed:
|
||||
display_name_failed = True
|
||||
extension_id = click.prompt("Extension ID", type=str)
|
||||
|
||||
|
||||
@app.command()
|
||||
@click.option(
|
||||
"--id",
|
||||
"id_opt",
|
||||
default=None,
|
||||
help="Extension ID (alphanumeric and underscores only)",
|
||||
help="Extension ID (kebab-case, e.g. hello-world)",
|
||||
)
|
||||
@click.option(
|
||||
"--name", "name_opt", default=None, help="Extension display name (e.g. Hello World)"
|
||||
)
|
||||
@click.option("--name", "name_opt", default=None, help="Extension display name")
|
||||
@click.option(
|
||||
"--version", "version_opt", default=None, help="Initial version (default: 0.1.0)"
|
||||
)
|
||||
@@ -431,18 +557,9 @@ def init(
|
||||
frontend_opt: bool | None,
|
||||
backend_opt: bool | None,
|
||||
) -> None:
|
||||
id_ = id_opt or click.prompt(
|
||||
"Extension ID (unique identifier, alphanumeric only)", type=str
|
||||
)
|
||||
if not re.match(r"^[a-zA-Z0-9_]+$", id_):
|
||||
click.secho(
|
||||
"❌ ID must be alphanumeric (letters, digits, underscore).", fg="red"
|
||||
)
|
||||
sys.exit(1)
|
||||
# Get extension names with graceful validation
|
||||
names = prompt_for_extension_name(name_opt, id_opt)
|
||||
|
||||
name = name_opt or click.prompt(
|
||||
"Extension name (human-readable display name)", type=str
|
||||
)
|
||||
version = version_opt or click.prompt("Initial version", default="0.1.0")
|
||||
license_ = license_opt or click.prompt("License", default="Apache-2.0")
|
||||
include_frontend = (
|
||||
@@ -456,7 +573,7 @@ def init(
|
||||
else click.confirm("Include backend?", default=True)
|
||||
)
|
||||
|
||||
target_dir = Path.cwd() / id_
|
||||
target_dir = Path.cwd() / names["id"]
|
||||
if target_dir.exists():
|
||||
click.secho(f"❌ Directory {target_dir} already exists.", fg="red")
|
||||
sys.exit(1)
|
||||
@@ -465,8 +582,7 @@ def init(
|
||||
templates_dir = Path(__file__).parent / "templates"
|
||||
env = Environment(loader=FileSystemLoader(templates_dir)) # noqa: S701
|
||||
ctx = {
|
||||
"id": id_,
|
||||
"name": name,
|
||||
**names, # Include all name variants
|
||||
"include_frontend": include_frontend,
|
||||
"include_backend": include_backend,
|
||||
"license": license_,
|
||||
@@ -502,29 +618,41 @@ def init(
|
||||
(frontend_src_dir / "index.tsx").write_text(index_tsx)
|
||||
click.secho("✅ Created frontend folder structure", fg="green")
|
||||
|
||||
# Initialize backend files
|
||||
# Initialize backend files with superset_extensions namespace
|
||||
if include_backend:
|
||||
backend_dir = target_dir / "backend"
|
||||
backend_dir.mkdir()
|
||||
backend_src_dir = backend_dir / "src"
|
||||
backend_src_dir.mkdir()
|
||||
backend_src_package_dir = backend_src_dir / id_
|
||||
backend_src_package_dir.mkdir()
|
||||
|
||||
# Create superset_extensions namespace directory
|
||||
namespace_dir = backend_src_dir / "superset_extensions"
|
||||
namespace_dir.mkdir()
|
||||
|
||||
# Create extension package directory
|
||||
extension_package_dir = namespace_dir / names["backend_name"]
|
||||
extension_package_dir.mkdir()
|
||||
|
||||
# backend files
|
||||
pyproject_toml = env.get_template("backend/pyproject.toml.j2").render(ctx)
|
||||
(backend_dir / "pyproject.toml").write_text(pyproject_toml)
|
||||
|
||||
# Namespace package __init__.py (empty for namespace)
|
||||
(namespace_dir / "__init__.py").write_text("")
|
||||
|
||||
# Extension package files
|
||||
init_py = env.get_template("backend/src/package/__init__.py.j2").render(ctx)
|
||||
(backend_src_package_dir / "__init__.py").write_text(init_py)
|
||||
(extension_package_dir / "__init__.py").write_text(init_py)
|
||||
entrypoint_py = env.get_template("backend/src/package/entrypoint.py.j2").render(
|
||||
ctx
|
||||
)
|
||||
(backend_src_package_dir / "entrypoint.py").write_text(entrypoint_py)
|
||||
(extension_package_dir / "entrypoint.py").write_text(entrypoint_py)
|
||||
|
||||
click.secho("✅ Created backend folder structure", fg="green")
|
||||
|
||||
click.secho(
|
||||
f"🎉 Extension {name} (ID: {id_}) initialized at {target_dir}", fg="cyan"
|
||||
f"🎉 Extension {names['name']} (ID: {names['id']}) initialized at {target_dir}",
|
||||
fg="cyan",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
class ExtensionNameError(Exception):
|
||||
"""Raised when extension name validation fails."""
|
||||
|
||||
pass
|
||||
@@ -1,4 +1,4 @@
|
||||
[project]
|
||||
name = "{{ id }}"
|
||||
name = "{{ backend_package }}"
|
||||
version = "{{ version }}"
|
||||
license = "{{ license }}"
|
||||
|
||||
@@ -1 +1 @@
|
||||
print("{{ name }} extension registered")
|
||||
print("{{ display_name }} extension registered")
|
||||
|
||||
@@ -11,14 +11,15 @@
|
||||
"menus": {}
|
||||
},
|
||||
"moduleFederation": {
|
||||
"name": "{{ mf_name }}",
|
||||
"exposes": ["./index"]
|
||||
}
|
||||
},
|
||||
{% endif -%}
|
||||
{% if include_backend -%}
|
||||
"backend": {
|
||||
"entryPoints": ["{{ id }}.entrypoint"],
|
||||
"files": ["backend/src/{{ id }}/**/*.py"]
|
||||
"entryPoints": ["{{ backend_entry }}"],
|
||||
"files": ["backend/src/superset_extensions/{{ backend_name }}/**/*.py"]
|
||||
},
|
||||
{% endif -%}
|
||||
"permissions": []
|
||||
|
||||
@@ -39,7 +39,7 @@ module.exports = (env, argv) => {
|
||||
},
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: "{{ id }}",
|
||||
name: "{{ mf_name }}",
|
||||
filename: "remoteEntry.[contenthash].js",
|
||||
exposes: {
|
||||
"./index": "./src/index.tsx",
|
||||
|
||||
40
superset-extensions-cli/src/superset_extensions_cli/types.py
Normal file
40
superset-extensions-cli/src/superset_extensions_cli/types.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class ExtensionNames(TypedDict):
|
||||
"""Type definition for extension name variants following platform conventions."""
|
||||
|
||||
# Extension name (e.g., "Hello World")
|
||||
name: str
|
||||
|
||||
# Extension ID - kebab-case primary identifier and npm package name (e.g., "hello-world")
|
||||
id: str
|
||||
|
||||
# Module Federation library - camelCase JS identifier (e.g., "helloWorld")
|
||||
mf_name: str
|
||||
|
||||
# Backend package name - snake_case (e.g., "hello_world")
|
||||
backend_name: str
|
||||
|
||||
# Full backend package (e.g., "superset_extensions.hello_world")
|
||||
backend_package: str
|
||||
|
||||
# Backend entry point (e.g., "superset_extensions.hello_world.entrypoint")
|
||||
backend_entry: str
|
||||
@@ -16,15 +16,75 @@
|
||||
# under the License.
|
||||
|
||||
import json # noqa: TID251
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from superset_extensions_cli.exceptions import ExtensionNameError
|
||||
from superset_extensions_cli.types import ExtensionNames
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
import tomli as tomllib
|
||||
|
||||
# Python reserved keywords to avoid in package names
|
||||
PYTHON_KEYWORDS = {
|
||||
"and",
|
||||
"as",
|
||||
"assert",
|
||||
"break",
|
||||
"class",
|
||||
"continue",
|
||||
"def",
|
||||
"del",
|
||||
"elif",
|
||||
"else",
|
||||
"except",
|
||||
"exec",
|
||||
"finally",
|
||||
"for",
|
||||
"from",
|
||||
"global",
|
||||
"if",
|
||||
"import",
|
||||
"in",
|
||||
"is",
|
||||
"lambda",
|
||||
"not",
|
||||
"or",
|
||||
"pass",
|
||||
"print",
|
||||
"raise",
|
||||
"return",
|
||||
"try",
|
||||
"while",
|
||||
"with",
|
||||
"yield",
|
||||
"False",
|
||||
"None",
|
||||
"True",
|
||||
}
|
||||
|
||||
# npm reserved names to avoid
|
||||
NPM_RESERVED = {
|
||||
"node_modules",
|
||||
"favicon.ico",
|
||||
"www",
|
||||
"http",
|
||||
"https",
|
||||
"ftp",
|
||||
"localhost",
|
||||
"package.json",
|
||||
"npm",
|
||||
"yarn",
|
||||
"bower_components",
|
||||
}
|
||||
|
||||
# Extension name pattern: lowercase, start with letter or number, alphanumeric + hyphens
|
||||
EXTENSION_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9]*(?:-[a-z0-9]+)*$")
|
||||
|
||||
|
||||
def read_toml(path: Path) -> dict[str, Any] | None:
|
||||
if not path.is_file():
|
||||
@@ -40,3 +100,205 @@ def read_json(path: Path) -> dict[str, Any] | None:
|
||||
return None
|
||||
|
||||
return json.loads(path.read_text())
|
||||
|
||||
|
||||
def _normalize_for_identifiers(name: str) -> str:
|
||||
"""
|
||||
Normalize display name to clean lowercase words.
|
||||
|
||||
Args:
|
||||
name: Raw display name (e.g., "Hello World!")
|
||||
|
||||
Returns:
|
||||
Normalized string (e.g., "hello world")
|
||||
"""
|
||||
# Convert to lowercase
|
||||
normalized = name.lower().strip()
|
||||
|
||||
# Convert underscores and existing hyphens to spaces for consistent processing
|
||||
normalized = normalized.replace("_", " ").replace("-", " ")
|
||||
|
||||
# Remove any non-alphanumeric characters except spaces
|
||||
normalized = re.sub(r"[^a-z0-9\s]", "", normalized)
|
||||
|
||||
# Normalize whitespace (collapse multiple spaces, strip)
|
||||
normalized = " ".join(normalized.split())
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalized_to_kebab(normalized: str) -> str:
|
||||
"""Convert normalized string to kebab-case."""
|
||||
return normalized.replace(" ", "-")
|
||||
|
||||
|
||||
def _normalized_to_snake(normalized: str) -> str:
|
||||
"""Convert normalized string to snake_case."""
|
||||
return normalized.replace(" ", "_")
|
||||
|
||||
|
||||
def _normalized_to_camel(normalized: str) -> str:
|
||||
"""Convert normalized string to camelCase."""
|
||||
parts = normalized.split()
|
||||
if not parts:
|
||||
return ""
|
||||
# First part lowercase, subsequent parts capitalized
|
||||
return parts[0] + "".join(word.capitalize() for word in parts[1:])
|
||||
|
||||
|
||||
def kebab_to_camel_case(kebab_name: str) -> str:
|
||||
"""Convert kebab-case to camelCase (e.g., 'hello-world' -> 'helloWorld')."""
|
||||
parts = kebab_name.split("-")
|
||||
if not parts:
|
||||
return ""
|
||||
# First part lowercase, subsequent parts capitalized
|
||||
return parts[0] + "".join(word.capitalize() for word in parts[1:])
|
||||
|
||||
|
||||
def kebab_to_snake_case(kebab_name: str) -> str:
|
||||
"""Convert kebab-case to snake_case (e.g., 'hello-world' -> 'hello_world')."""
|
||||
return kebab_name.replace("-", "_")
|
||||
|
||||
|
||||
def name_to_kebab_case(name: str) -> str:
|
||||
"""Convert display name directly to kebab-case (e.g., 'Hello World' -> 'hello-world')."""
|
||||
normalized = _normalize_for_identifiers(name)
|
||||
return _normalized_to_kebab(normalized)
|
||||
|
||||
|
||||
# Legacy functions for backward compatibility
|
||||
def to_kebab_case(name: str) -> str:
|
||||
"""Convert display name to kebab-case. For new code, use name_to_kebab_case."""
|
||||
return name_to_kebab_case(name)
|
||||
|
||||
|
||||
def to_snake_case(kebab_name: str) -> str:
|
||||
"""Convert kebab-case to snake_case. For new code, use kebab_to_snake_case."""
|
||||
return kebab_to_snake_case(kebab_name)
|
||||
|
||||
|
||||
def validate_extension_id(extension_id: str) -> None:
|
||||
"""
|
||||
Validate extension ID format (kebab-case).
|
||||
|
||||
Raises:
|
||||
ExtensionNameError: If ID is invalid
|
||||
"""
|
||||
if not extension_id:
|
||||
raise ExtensionNameError("Extension ID cannot be empty")
|
||||
|
||||
# Check for leading/trailing hyphens first
|
||||
if extension_id.startswith("-"):
|
||||
raise ExtensionNameError("Extension ID cannot start with hyphens")
|
||||
|
||||
if extension_id.endswith("-"):
|
||||
raise ExtensionNameError("Extension ID cannot end with hyphens")
|
||||
|
||||
# Check for consecutive hyphens
|
||||
if "--" in extension_id:
|
||||
raise ExtensionNameError("Extension ID cannot have consecutive hyphens")
|
||||
|
||||
# Check overall pattern
|
||||
if not EXTENSION_NAME_PATTERN.match(extension_id):
|
||||
raise ExtensionNameError(
|
||||
"Use lowercase letters, numbers, and hyphens only (e.g. hello-world)"
|
||||
)
|
||||
|
||||
|
||||
def validate_extension_name(name: str) -> str:
|
||||
"""
|
||||
Validate and normalize extension name (human-readable).
|
||||
|
||||
Args:
|
||||
extension_name: Raw extension name input
|
||||
|
||||
Returns:
|
||||
Cleaned extension name
|
||||
|
||||
Raises:
|
||||
ExtensionNameError: If extension name is invalid
|
||||
"""
|
||||
if not name or not name.strip():
|
||||
raise ExtensionNameError("Extension name cannot be empty")
|
||||
|
||||
# Normalize whitespace: strip and collapse multiple spaces
|
||||
normalized = " ".join(name.strip().split())
|
||||
|
||||
# Check for only whitespace/special chars after normalization
|
||||
if not any(c.isalnum() for c in normalized):
|
||||
raise ExtensionNameError(
|
||||
"Extension name must contain at least one letter or number"
|
||||
)
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def validate_python_package_name(name: str) -> None:
|
||||
"""
|
||||
Validate Python package name (snake_case format).
|
||||
|
||||
Raises:
|
||||
ExtensionNameError: If name is invalid
|
||||
"""
|
||||
# Check if it starts with a number (invalid for Python identifiers)
|
||||
if name[0].isdigit():
|
||||
raise ExtensionNameError(f"Package name '{name}' cannot start with a number")
|
||||
|
||||
# Check if the first part (before any underscore) is a Python keyword
|
||||
if (first_part := name.split("_")[0]) in PYTHON_KEYWORDS:
|
||||
raise ExtensionNameError(
|
||||
f"Package name cannot start with Python keyword '{first_part}'"
|
||||
)
|
||||
|
||||
# Check if it's a valid Python identifier
|
||||
if not name.replace("_", "a").isalnum():
|
||||
raise ExtensionNameError(f"'{name}' is not a valid Python package name")
|
||||
|
||||
|
||||
def validate_npm_package_name(name: str) -> None:
|
||||
"""
|
||||
Validate npm package name (kebab-case format).
|
||||
|
||||
Raises:
|
||||
ExtensionNameError: If name is invalid
|
||||
"""
|
||||
if name.lower() in NPM_RESERVED:
|
||||
raise ExtensionNameError(f"'{name}' is a reserved npm package name")
|
||||
|
||||
|
||||
def generate_extension_names(name: str) -> ExtensionNames:
|
||||
"""
|
||||
Generate all extension name variants from display name input.
|
||||
|
||||
Flow: Display Name -> Generate ID -> Derive Technical Names from ID
|
||||
Example: "Hello World" -> "hello-world" -> "helloWorld"/"hello_world" (from ID)
|
||||
|
||||
Returns:
|
||||
ExtensionNames: Dictionary with all name variants
|
||||
|
||||
Raises:
|
||||
ExtensionNameError: If any generated name is invalid
|
||||
"""
|
||||
# Validate and normalize the extension name
|
||||
name = validate_extension_name(name)
|
||||
|
||||
# Generate ID from display name
|
||||
kebab_name = name_to_kebab_case(name)
|
||||
|
||||
# Derive all technical names from the generated ID (not display name)
|
||||
snake_name = kebab_to_snake_case(kebab_name)
|
||||
camel_name = kebab_to_camel_case(kebab_name)
|
||||
|
||||
# Validate the generated names
|
||||
validate_extension_id(kebab_name)
|
||||
validate_python_package_name(snake_name)
|
||||
validate_npm_package_name(kebab_name)
|
||||
|
||||
return ExtensionNames(
|
||||
name=name,
|
||||
id=kebab_name,
|
||||
mf_name=camel_name,
|
||||
backend_name=snake_name,
|
||||
backend_package=f"superset_extensions.{snake_name}",
|
||||
backend_entry=f"superset_extensions.{snake_name}.entrypoint",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user