mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
fix: Rename apache-superset-cli to apache-superset-extensions-cli (#34883)
This commit is contained in:
committed by
GitHub
parent
ebfb14c353
commit
bcf156c969
@@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
471
superset-extensions-cli/src/superset_extensions_cli/cli.py
Normal file
471
superset-extensions-cli/src/superset_extensions_cli/cli.py
Normal file
@@ -0,0 +1,471 @@
|
||||
# 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.
|
||||
|
||||
import json # noqa: TID251
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, cast
|
||||
|
||||
import click
|
||||
import semver
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from superset_core.extensions.types import Manifest, Metadata
|
||||
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
|
||||
|
||||
REMOTE_ENTRY_REGEX = re.compile(r"^remoteEntry\..+\.js$")
|
||||
FRONTEND_DIST_REGEX = re.compile(r"/frontend/dist")
|
||||
|
||||
|
||||
def validate_npm() -> None:
|
||||
"""Abort if `npm` is not on PATH."""
|
||||
if shutil.which("npm") is None:
|
||||
click.secho(
|
||||
"❌ npm is not installed or not on your PATH.",
|
||||
err=True,
|
||||
fg="red",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
result = subprocess.run( # noqa: S603
|
||||
["npm", "-v"], # noqa: S607
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
click.secho(
|
||||
f"❌ Failed to run `npm -v`: {result.stderr.strip()}",
|
||||
err=True,
|
||||
fg="red",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
npm_version = result.stdout.strip()
|
||||
if semver.compare(npm_version, MIN_NPM_VERSION) < 0:
|
||||
click.secho(
|
||||
f"❌ npm version {npm_version} is lower than the required {MIN_NPM_VERSION}.", # noqa: E501
|
||||
err=True,
|
||||
fg="red",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
except FileNotFoundError:
|
||||
click.secho(
|
||||
"❌ npm was not found when checking its version.",
|
||||
err=True,
|
||||
fg="red",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def init_frontend_deps(frontend_dir: Path) -> None:
|
||||
"""
|
||||
If node_modules is missing under `frontend_dir`, run `npm ci` if package-lock.json
|
||||
exists, otherwise run `npm i`.
|
||||
"""
|
||||
node_modules = frontend_dir / "node_modules"
|
||||
if not node_modules.exists():
|
||||
package_lock = frontend_dir / "package-lock.json"
|
||||
if package_lock.exists():
|
||||
click.secho("⚙️ node_modules not found, running `npm ci`…", fg="cyan")
|
||||
npm_command = ["npm", "ci"]
|
||||
error_msg = "❌ `npm ci` failed. Aborting."
|
||||
else:
|
||||
click.secho("⚙️ node_modules not found, running `npm i`…", fg="cyan")
|
||||
npm_command = ["npm", "i"]
|
||||
error_msg = "❌ `npm i` failed. Aborting."
|
||||
|
||||
validate_npm()
|
||||
res = subprocess.run( # noqa: S603
|
||||
npm_command, # noqa: S607
|
||||
cwd=frontend_dir,
|
||||
text=True,
|
||||
)
|
||||
if res.returncode != 0:
|
||||
click.secho(error_msg, err=True, fg="red")
|
||||
sys.exit(1)
|
||||
click.secho("✅ Dependencies installed", fg="green")
|
||||
|
||||
|
||||
def clean_dist(cwd: Path) -> None:
|
||||
dist_dir = cwd / "dist"
|
||||
if dist_dir.exists():
|
||||
shutil.rmtree(dist_dir)
|
||||
dist_dir.mkdir(parents=True)
|
||||
|
||||
|
||||
def clean_dist_frontend(cwd: Path) -> None:
|
||||
frontend_dist = cwd / "dist" / "frontend"
|
||||
if frontend_dist.exists():
|
||||
shutil.rmtree(frontend_dist)
|
||||
|
||||
|
||||
def build_manifest(cwd: Path, remote_entry: str | None) -> Manifest:
|
||||
extension: Metadata = cast(Metadata, read_json(cwd / "extension.json"))
|
||||
if not extension:
|
||||
click.secho("❌ extension.json not found.", err=True, fg="red")
|
||||
sys.exit(1)
|
||||
|
||||
manifest: Manifest = {
|
||||
"id": extension["id"],
|
||||
"name": extension["name"],
|
||||
"version": extension["version"],
|
||||
"permissions": extension["permissions"],
|
||||
"dependencies": extension.get("dependencies", []),
|
||||
}
|
||||
if (
|
||||
(frontend := extension.get("frontend"))
|
||||
and (contributions := frontend.get("contributions"))
|
||||
and (module_federation := frontend.get("moduleFederation"))
|
||||
and remote_entry
|
||||
):
|
||||
manifest["frontend"] = {
|
||||
"contributions": contributions,
|
||||
"moduleFederation": module_federation,
|
||||
"remoteEntry": remote_entry,
|
||||
}
|
||||
|
||||
if entry_points := extension.get("backend", {}).get("entryPoints"):
|
||||
manifest["backend"] = {"entryPoints": entry_points}
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def write_manifest(cwd: Path, manifest: Manifest) -> None:
|
||||
dist_dir = cwd / "dist"
|
||||
(dist_dir / "manifest.json").write_text(
|
||||
json.dumps(manifest, indent=2, sort_keys=True)
|
||||
)
|
||||
click.secho("✅ Manifest updated", fg="green")
|
||||
|
||||
|
||||
def run_frontend_build(frontend_dir: Path) -> subprocess.CompletedProcess[str]:
|
||||
click.echo()
|
||||
click.secho("⚙️ Building frontend assets…", fg="cyan")
|
||||
return subprocess.run( # noqa: S603
|
||||
["npm", "run", "build"], # noqa: S607
|
||||
cwd=frontend_dir,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def copy_frontend_dist(cwd: Path) -> str:
|
||||
dist_dir = cwd / "dist"
|
||||
frontend_dist = cwd / "frontend" / "dist"
|
||||
remote_entry: str | None = None
|
||||
|
||||
for f in frontend_dist.rglob("*"):
|
||||
if not f.is_file():
|
||||
continue
|
||||
if REMOTE_ENTRY_REGEX.match(f.name):
|
||||
remote_entry = f.name
|
||||
tgt = dist_dir / f.relative_to(cwd)
|
||||
tgt.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(f, tgt)
|
||||
|
||||
if not remote_entry:
|
||||
click.secho("❌ No remote entry file found.", err=True, fg="red")
|
||||
sys.exit(1)
|
||||
return remote_entry
|
||||
|
||||
|
||||
def copy_backend_files(cwd: Path) -> None:
|
||||
dist_dir = cwd / "dist"
|
||||
extension = read_json(cwd / "extension.json")
|
||||
if not extension:
|
||||
click.secho("❌ No extension.json file found.", err=True, fg="red")
|
||||
sys.exit(1)
|
||||
|
||||
for pat in extension.get("backend", {}).get("files", []):
|
||||
for f in cwd.glob(pat):
|
||||
if not f.is_file():
|
||||
continue
|
||||
tgt = dist_dir / f.relative_to(cwd)
|
||||
tgt.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(f, tgt)
|
||||
|
||||
|
||||
def rebuild_frontend(cwd: Path, frontend_dir: Path) -> str | None:
|
||||
"""Clean and rebuild frontend, return the remoteEntry filename."""
|
||||
clean_dist_frontend(cwd)
|
||||
|
||||
res = run_frontend_build(frontend_dir)
|
||||
if res.returncode != 0:
|
||||
click.secho("❌ Frontend build failed", fg="red")
|
||||
return None
|
||||
|
||||
remote_entry = copy_frontend_dist(cwd)
|
||||
click.secho("✅ Frontend rebuilt", fg="green")
|
||||
return remote_entry
|
||||
|
||||
|
||||
def rebuild_backend(cwd: Path) -> None:
|
||||
"""Copy backend files (no manifest update)."""
|
||||
copy_backend_files(cwd)
|
||||
click.secho("✅ Backend files synced", fg="green")
|
||||
|
||||
|
||||
class FrontendChangeHandler(FileSystemEventHandler):
|
||||
def __init__(self, trigger_build: Callable[[], None]):
|
||||
self.trigger_build = trigger_build
|
||||
|
||||
def on_any_event(self, event: Any) -> None:
|
||||
if FRONTEND_DIST_REGEX.search(event.src_path):
|
||||
return
|
||||
click.secho(f"🔁 Frontend change detected: {event.src_path}", fg="yellow")
|
||||
self.trigger_build()
|
||||
|
||||
|
||||
@click.group(help="CLI for validating and bundling Superset extensions.")
|
||||
def app() -> None:
|
||||
pass
|
||||
|
||||
|
||||
@app.command()
|
||||
def validate() -> None:
|
||||
validate_npm()
|
||||
|
||||
click.secho("✅ Validation successful", fg="green")
|
||||
|
||||
|
||||
@app.command()
|
||||
@click.pass_context
|
||||
def build(ctx: click.Context) -> None:
|
||||
ctx.invoke(validate)
|
||||
cwd = Path.cwd()
|
||||
frontend_dir = cwd / "frontend"
|
||||
backend_dir = cwd / "backend"
|
||||
|
||||
clean_dist(cwd)
|
||||
|
||||
# Build frontend if it exists
|
||||
remote_entry = None
|
||||
if frontend_dir.exists():
|
||||
init_frontend_deps(frontend_dir)
|
||||
remote_entry = rebuild_frontend(cwd, frontend_dir)
|
||||
|
||||
# Build backend independently if it exists
|
||||
if backend_dir.exists():
|
||||
pyproject = read_toml(backend_dir / "pyproject.toml")
|
||||
if pyproject:
|
||||
rebuild_backend(cwd)
|
||||
|
||||
# Build manifest and write it
|
||||
manifest = build_manifest(cwd, remote_entry)
|
||||
write_manifest(cwd, manifest)
|
||||
|
||||
click.secho("✅ Full build completed in dist/", fg="green")
|
||||
|
||||
|
||||
@app.command()
|
||||
@click.option(
|
||||
"--output",
|
||||
"-o",
|
||||
type=click.Path(path_type=Path, dir_okay=True, file_okay=True, writable=True),
|
||||
help="Optional output path or filename for the bundle.",
|
||||
)
|
||||
@click.pass_context
|
||||
def bundle(ctx: click.Context, output: Path | None) -> None:
|
||||
ctx.invoke(build)
|
||||
|
||||
cwd = Path.cwd()
|
||||
dist_dir = cwd / "dist"
|
||||
manifest_path = dist_dir / "manifest.json"
|
||||
|
||||
if not manifest_path.exists():
|
||||
click.secho(
|
||||
"❌ dist/manifest.json not found. Run `build` first.", err=True, fg="red"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
id_ = manifest["id"]
|
||||
version = manifest["version"]
|
||||
default_filename = f"{id_}-{version}.supx"
|
||||
|
||||
if output is None:
|
||||
zip_path = Path(default_filename)
|
||||
elif output.is_dir():
|
||||
zip_path = output / default_filename
|
||||
else:
|
||||
zip_path = output
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file in dist_dir.rglob("*"):
|
||||
if file.is_file():
|
||||
arcname = file.relative_to(dist_dir)
|
||||
zipf.write(file, arcname)
|
||||
except Exception as ex:
|
||||
click.secho(f"❌ Failed to create bundle: {ex}", err=True, fg="red")
|
||||
sys.exit(1)
|
||||
|
||||
click.secho(f"✅ Bundle created: {zip_path}", fg="green")
|
||||
|
||||
|
||||
@app.command()
|
||||
@click.pass_context
|
||||
def dev(ctx: click.Context) -> None:
|
||||
cwd = Path.cwd()
|
||||
frontend_dir = cwd / "frontend"
|
||||
backend_dir = cwd / "backend"
|
||||
|
||||
clean_dist(cwd)
|
||||
|
||||
# Build frontend if it exists
|
||||
remote_entry = None
|
||||
if frontend_dir.exists():
|
||||
init_frontend_deps(frontend_dir)
|
||||
remote_entry = rebuild_frontend(cwd, frontend_dir)
|
||||
|
||||
# Build backend if it exists
|
||||
if backend_dir.exists():
|
||||
rebuild_backend(cwd)
|
||||
|
||||
manifest = build_manifest(cwd, remote_entry)
|
||||
write_manifest(cwd, manifest)
|
||||
|
||||
def frontend_watcher() -> None:
|
||||
if frontend_dir.exists():
|
||||
if (remote_entry := rebuild_frontend(cwd, frontend_dir)) is not None:
|
||||
manifest = build_manifest(cwd, remote_entry)
|
||||
write_manifest(cwd, manifest)
|
||||
|
||||
def backend_watcher() -> None:
|
||||
if backend_dir.exists():
|
||||
rebuild_backend(cwd)
|
||||
dist_dir = cwd / "dist"
|
||||
manifest_path = dist_dir / "manifest.json"
|
||||
if manifest_path.exists():
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
write_manifest(cwd, manifest)
|
||||
|
||||
# Build watch message based on existing directories
|
||||
watch_dirs = []
|
||||
if frontend_dir.exists():
|
||||
watch_dirs.append(str(frontend_dir))
|
||||
if backend_dir.exists():
|
||||
watch_dirs.append(str(backend_dir))
|
||||
|
||||
if watch_dirs:
|
||||
click.secho(f"👀 Watching for changes in: {', '.join(watch_dirs)}", fg="green")
|
||||
else:
|
||||
click.secho("⚠️ No frontend or backend directories found to watch", fg="yellow")
|
||||
|
||||
observer = Observer()
|
||||
|
||||
# Only set up watchers for directories that exist
|
||||
if frontend_dir.exists():
|
||||
frontend_handler = FrontendChangeHandler(trigger_build=frontend_watcher)
|
||||
observer.schedule(frontend_handler, str(frontend_dir), recursive=True)
|
||||
|
||||
if backend_dir.exists():
|
||||
backend_handler = FileSystemEventHandler()
|
||||
backend_handler.on_any_event = lambda event: backend_watcher()
|
||||
observer.schedule(backend_handler, str(backend_dir), recursive=True)
|
||||
|
||||
if watch_dirs:
|
||||
observer.start()
|
||||
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
click.secho("\n🛑 Stopping watch mode", fg="blue")
|
||||
observer.stop()
|
||||
|
||||
observer.join()
|
||||
else:
|
||||
click.secho("❌ No directories to watch. Exiting.", fg="red")
|
||||
|
||||
|
||||
@app.command()
|
||||
def init() -> None:
|
||||
id_ = 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)
|
||||
|
||||
name = click.prompt("Extension name (human-readable display name)", type=str)
|
||||
version = click.prompt("Initial version", default="0.1.0")
|
||||
license = click.prompt("License", default="Apache-2.0")
|
||||
include_frontend = click.confirm("Include frontend?", default=True)
|
||||
include_backend = click.confirm("Include backend?", default=True)
|
||||
|
||||
target_dir = Path.cwd() / id_
|
||||
if target_dir.exists():
|
||||
click.secho(f"❌ Directory {target_dir} already exists.", fg="red")
|
||||
sys.exit(1)
|
||||
|
||||
# Set up Jinja environment
|
||||
templates_dir = Path(__file__).parent / "templates"
|
||||
env = Environment(loader=FileSystemLoader(templates_dir)) # noqa: S701
|
||||
ctx = {
|
||||
"id": id_,
|
||||
"name": name,
|
||||
"include_frontend": include_frontend,
|
||||
"include_backend": include_backend,
|
||||
"license": license,
|
||||
"version": version,
|
||||
}
|
||||
|
||||
# Create base directory
|
||||
target_dir.mkdir()
|
||||
extension_json = env.get_template("extension.json.j2").render(ctx)
|
||||
(target_dir / "extension.json").write_text(extension_json)
|
||||
click.secho("✅ Created extension.json", fg="green")
|
||||
|
||||
# Copy frontend template
|
||||
if include_frontend:
|
||||
frontend_dir = target_dir / "frontend"
|
||||
frontend_dir.mkdir()
|
||||
|
||||
# package.json
|
||||
package_json = env.get_template("frontend/package.json.j2").render(ctx)
|
||||
(frontend_dir / "package.json").write_text(package_json)
|
||||
click.secho("✅ Created frontend folder structure", fg="green")
|
||||
|
||||
# Copy backend template
|
||||
if include_backend:
|
||||
backend_dir = target_dir / "backend"
|
||||
backend_dir.mkdir()
|
||||
|
||||
# pyproject.toml
|
||||
pyproject_toml = env.get_template("backend/pyproject.toml.j2").render(ctx)
|
||||
(backend_dir / "pyproject.toml").write_text(pyproject_toml)
|
||||
|
||||
click.secho("✅ Created backend folder structure", fg="green")
|
||||
|
||||
click.secho(
|
||||
f"🎉 Extension {name} (ID: {id_}) initialized at {target_dir}", fg="cyan"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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.
|
||||
|
||||
|
||||
MIN_NPM_VERSION = "10.8.2"
|
||||
@@ -0,0 +1,4 @@
|
||||
[project]
|
||||
name = "{{ id }}"
|
||||
version = "{{ version }}"
|
||||
license = "{{ license }}"
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "{{ id }}",
|
||||
"name": "{{ name }}",
|
||||
"version": "{{ version }}",
|
||||
"license": "{{ license }}",
|
||||
{% if include_frontend -%}
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"commands": [],
|
||||
"views": [],
|
||||
"menus": []
|
||||
},
|
||||
"moduleFederation": {
|
||||
"exposes": ["./index"]
|
||||
}
|
||||
},
|
||||
{% endif -%}
|
||||
{% if include_backend -%}
|
||||
"backend": {
|
||||
"entryPoints": ["{{ id }}.entrypoint"],
|
||||
"files": ["backend/src/{{ id }}/**/*.py"]
|
||||
},
|
||||
{% endif -%}
|
||||
"permissions": []
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "{{ id }}",
|
||||
"version": "{{ version }}",
|
||||
"main": "dist/main.js",
|
||||
"types": "dist/publicAPI.d.ts",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "webpack serve --mode development",
|
||||
"build": "webpack --stats-error-details --mode production"
|
||||
},
|
||||
"keywords": [],
|
||||
"private": true,
|
||||
"author": "",
|
||||
"license": "{{ license }}",
|
||||
"description": "",
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "file:../../../superset-frontend/packages/superset-core",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"install": "^0.13.0",
|
||||
"npm": "^11.1.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.8.2",
|
||||
"webpack": "^5.98.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.0"
|
||||
}
|
||||
}
|
||||
42
superset-extensions-cli/src/superset_extensions_cli/utils.py
Normal file
42
superset-extensions-cli/src/superset_extensions_cli/utils.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# 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.
|
||||
|
||||
import json # noqa: TID251
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
def read_toml(path: Path) -> dict[str, Any] | None:
|
||||
if not path.is_file():
|
||||
return None
|
||||
|
||||
with path.open("rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict[str, Any] | None:
|
||||
path = Path(path)
|
||||
if not path.is_file():
|
||||
return None
|
||||
|
||||
return json.loads(path.read_text())
|
||||
Reference in New Issue
Block a user