kopia lustrzana https://github.com/simonw/datasette
253 wiersze
7.7 KiB
Python
253 wiersze
7.7 KiB
Python
from contextlib import contextmanager
|
|
from datasette import hookimpl
|
|
import click
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import shlex
|
|
import shutil
|
|
from subprocess import call, check_output
|
|
import tempfile
|
|
|
|
from .common import (
|
|
add_common_publish_arguments_and_options,
|
|
fail_if_publish_binary_not_installed,
|
|
)
|
|
from datasette.utils import link_or_copy, link_or_copy_directory, parse_metadata
|
|
|
|
|
|
@hookimpl
|
|
def publish_subcommand(publish):
|
|
@publish.command()
|
|
@add_common_publish_arguments_and_options
|
|
@click.option(
|
|
"-n",
|
|
"--name",
|
|
default="datasette",
|
|
help="Application name to use when deploying",
|
|
)
|
|
@click.option(
|
|
"--tar",
|
|
help="--tar option to pass to Heroku, e.g. --tar=/usr/local/bin/gtar",
|
|
)
|
|
@click.option(
|
|
"--generate-dir",
|
|
type=click.Path(dir_okay=True, file_okay=False),
|
|
help="Output generated application files and stop without deploying",
|
|
)
|
|
def heroku(
|
|
files,
|
|
metadata,
|
|
extra_options,
|
|
branch,
|
|
template_dir,
|
|
plugins_dir,
|
|
static,
|
|
install,
|
|
plugin_secret,
|
|
version_note,
|
|
secret,
|
|
title,
|
|
license,
|
|
license_url,
|
|
source,
|
|
source_url,
|
|
about,
|
|
about_url,
|
|
name,
|
|
tar,
|
|
generate_dir,
|
|
):
|
|
"Publish databases to Datasette running on Heroku"
|
|
fail_if_publish_binary_not_installed(
|
|
"heroku", "Heroku", "https://cli.heroku.com"
|
|
)
|
|
|
|
# Check for heroku-builds plugin
|
|
plugins = [
|
|
line.split()[0] for line in check_output(["heroku", "plugins"]).splitlines()
|
|
]
|
|
if b"heroku-builds" not in plugins:
|
|
click.echo(
|
|
"Publishing to Heroku requires the heroku-builds plugin to be installed."
|
|
)
|
|
click.confirm(
|
|
"Install it? (this will run `heroku plugins:install heroku-builds`)",
|
|
abort=True,
|
|
)
|
|
call(["heroku", "plugins:install", "heroku-builds"])
|
|
|
|
extra_metadata = {
|
|
"title": title,
|
|
"license": license,
|
|
"license_url": license_url,
|
|
"source": source,
|
|
"source_url": source_url,
|
|
"about": about,
|
|
"about_url": about_url,
|
|
}
|
|
|
|
environment_variables = {}
|
|
if plugin_secret:
|
|
extra_metadata["plugins"] = {}
|
|
for plugin_name, plugin_setting, setting_value in plugin_secret:
|
|
environment_variable = (
|
|
f"{plugin_name}_{plugin_setting}".upper().replace("-", "_")
|
|
)
|
|
environment_variables[environment_variable] = setting_value
|
|
extra_metadata["plugins"].setdefault(plugin_name, {})[
|
|
plugin_setting
|
|
] = {"$env": environment_variable}
|
|
|
|
with temporary_heroku_directory(
|
|
files,
|
|
name,
|
|
metadata,
|
|
extra_options,
|
|
branch,
|
|
template_dir,
|
|
plugins_dir,
|
|
static,
|
|
install,
|
|
version_note,
|
|
secret,
|
|
extra_metadata,
|
|
):
|
|
if generate_dir:
|
|
# Recursively copy files from current working directory to it
|
|
if pathlib.Path(generate_dir).exists():
|
|
raise click.ClickException("Directory already exists")
|
|
shutil.copytree(".", generate_dir)
|
|
click.echo(
|
|
f"Generated files written to {generate_dir}, stopping without deploying",
|
|
err=True,
|
|
)
|
|
return
|
|
app_name = None
|
|
if name:
|
|
# Check to see if this app already exists
|
|
list_output = check_output(["heroku", "apps:list", "--json"]).decode(
|
|
"utf8"
|
|
)
|
|
apps = json.loads(list_output)
|
|
|
|
for app in apps:
|
|
if app["name"] == name:
|
|
app_name = name
|
|
break
|
|
|
|
if not app_name:
|
|
# Create a new app
|
|
cmd = ["heroku", "apps:create"]
|
|
if name:
|
|
cmd.append(name)
|
|
cmd.append("--json")
|
|
create_output = check_output(cmd).decode("utf8")
|
|
app_name = json.loads(create_output)["name"]
|
|
|
|
for key, value in environment_variables.items():
|
|
call(["heroku", "config:set", "-a", app_name, f"{key}={value}"])
|
|
tar_option = []
|
|
if tar:
|
|
tar_option = ["--tar", tar]
|
|
call(
|
|
["heroku", "builds:create", "-a", app_name, "--include-vcs-ignore"]
|
|
+ tar_option
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def temporary_heroku_directory(
|
|
files,
|
|
name,
|
|
metadata,
|
|
extra_options,
|
|
branch,
|
|
template_dir,
|
|
plugins_dir,
|
|
static,
|
|
install,
|
|
version_note,
|
|
secret,
|
|
extra_metadata=None,
|
|
):
|
|
extra_metadata = extra_metadata or {}
|
|
tmp = tempfile.TemporaryDirectory()
|
|
saved_cwd = os.getcwd()
|
|
|
|
file_paths = [os.path.join(saved_cwd, file_path) for file_path in files]
|
|
file_names = [os.path.split(f)[-1] for f in files]
|
|
|
|
if metadata:
|
|
metadata_content = parse_metadata(metadata.read())
|
|
else:
|
|
metadata_content = {}
|
|
for key, value in extra_metadata.items():
|
|
if value:
|
|
metadata_content[key] = value
|
|
|
|
try:
|
|
os.chdir(tmp.name)
|
|
|
|
if metadata_content:
|
|
with open("metadata.json", "w") as fp:
|
|
fp.write(json.dumps(metadata_content, indent=2))
|
|
|
|
with open("runtime.txt", "w") as fp:
|
|
fp.write("python-3.11.0")
|
|
|
|
if branch:
|
|
install = [
|
|
f"https://github.com/simonw/datasette/archive/{branch}.zip"
|
|
] + list(install)
|
|
else:
|
|
install = ["datasette"] + list(install)
|
|
|
|
with open("requirements.txt", "w") as fp:
|
|
fp.write("\n".join(install))
|
|
os.mkdir("bin")
|
|
with open("bin/post_compile", "w") as fp:
|
|
fp.write("datasette inspect --inspect-file inspect-data.json")
|
|
|
|
extras = []
|
|
if template_dir:
|
|
link_or_copy_directory(
|
|
os.path.join(saved_cwd, template_dir),
|
|
os.path.join(tmp.name, "templates"),
|
|
)
|
|
extras.extend(["--template-dir", "templates/"])
|
|
if plugins_dir:
|
|
link_or_copy_directory(
|
|
os.path.join(saved_cwd, plugins_dir), os.path.join(tmp.name, "plugins")
|
|
)
|
|
extras.extend(["--plugins-dir", "plugins/"])
|
|
if version_note:
|
|
extras.extend(["--version-note", version_note])
|
|
if metadata_content:
|
|
extras.extend(["--metadata", "metadata.json"])
|
|
if extra_options:
|
|
extras.extend(extra_options.split())
|
|
for mount_point, path in static:
|
|
link_or_copy_directory(
|
|
os.path.join(saved_cwd, path), os.path.join(tmp.name, mount_point)
|
|
)
|
|
extras.extend(["--static", f"{mount_point}:{mount_point}"])
|
|
|
|
quoted_files = " ".join(
|
|
["-i {}".format(shlex.quote(file_name)) for file_name in file_names]
|
|
)
|
|
procfile_cmd = "web: datasette serve --host 0.0.0.0 {quoted_files} --cors --port $PORT --inspect-file inspect-data.json {extras}".format(
|
|
quoted_files=quoted_files, extras=" ".join(extras)
|
|
)
|
|
with open("Procfile", "w") as fp:
|
|
fp.write(procfile_cmd)
|
|
|
|
for path, filename in zip(file_paths, file_names):
|
|
link_or_copy(path, os.path.join(tmp.name, filename))
|
|
|
|
yield
|
|
|
|
finally:
|
|
tmp.cleanup()
|
|
os.chdir(saved_cwd)
|