Coverage for datasette/publish/heroku.py : 86%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from contextlib import contextmanager
2from datasette import hookimpl
3import click
4import json
5import os
6import shlex
7from subprocess import call, check_output
8import tempfile
10from .common import (
11 add_common_publish_arguments_and_options,
12 fail_if_publish_binary_not_installed,
13)
14from datasette.utils import link_or_copy, link_or_copy_directory, parse_metadata
17@hookimpl
18def publish_subcommand(publish):
19 @publish.command()
20 @add_common_publish_arguments_and_options
21 @click.option(
22 "-n",
23 "--name",
24 default="datasette",
25 help="Application name to use when deploying",
26 )
27 def heroku(
28 files,
29 metadata,
30 extra_options,
31 branch,
32 template_dir,
33 plugins_dir,
34 static,
35 install,
36 plugin_secret,
37 version_note,
38 secret,
39 title,
40 license,
41 license_url,
42 source,
43 source_url,
44 about,
45 about_url,
46 name,
47 ):
48 fail_if_publish_binary_not_installed(
49 "heroku", "Heroku", "https://cli.heroku.com"
50 )
52 # Check for heroku-builds plugin
53 plugins = [
54 line.split()[0] for line in check_output(["heroku", "plugins"]).splitlines()
55 ]
56 if b"heroku-builds" not in plugins:
57 click.echo(
58 "Publishing to Heroku requires the heroku-builds plugin to be installed."
59 )
60 click.confirm(
61 "Install it? (this will run `heroku plugins:install heroku-builds`)",
62 abort=True,
63 )
64 call(["heroku", "plugins:install", "heroku-builds"])
66 extra_metadata = {
67 "title": title,
68 "license": license,
69 "license_url": license_url,
70 "source": source,
71 "source_url": source_url,
72 "about": about,
73 "about_url": about_url,
74 }
76 environment_variables = {
77 # Avoid uvicorn error: https://github.com/simonw/datasette/issues/633
78 "WEB_CONCURRENCY": "1"
79 }
80 if plugin_secret:
81 extra_metadata["plugins"] = {}
82 for plugin_name, plugin_setting, setting_value in plugin_secret:
83 environment_variable = (
84 "{}_{}".format(plugin_name, plugin_setting)
85 .upper()
86 .replace("-", "_")
87 )
88 environment_variables[environment_variable] = setting_value
89 extra_metadata["plugins"].setdefault(plugin_name, {})[
90 plugin_setting
91 ] = {"$env": environment_variable}
93 with temporary_heroku_directory(
94 files,
95 name,
96 metadata,
97 extra_options,
98 branch,
99 template_dir,
100 plugins_dir,
101 static,
102 install,
103 version_note,
104 secret,
105 extra_metadata,
106 ):
107 app_name = None
108 if name:
109 # Check to see if this app already exists
110 list_output = check_output(["heroku", "apps:list", "--json"]).decode(
111 "utf8"
112 )
113 apps = json.loads(list_output)
115 for app in apps:
116 if app["name"] == name:
117 app_name = name
118 break
120 if not app_name:
121 # Create a new app
122 cmd = ["heroku", "apps:create"]
123 if name:
124 cmd.append(name)
125 cmd.append("--json")
126 create_output = check_output(cmd).decode("utf8")
127 app_name = json.loads(create_output)["name"]
129 for key, value in environment_variables.items():
130 call(
131 ["heroku", "config:set", "-a", app_name, "{}={}".format(key, value)]
132 )
134 call(["heroku", "builds:create", "-a", app_name, "--include-vcs-ignore"])
137@contextmanager
138def temporary_heroku_directory(
139 files,
140 name,
141 metadata,
142 extra_options,
143 branch,
144 template_dir,
145 plugins_dir,
146 static,
147 install,
148 version_note,
149 secret,
150 extra_metadata=None,
151):
152 extra_metadata = extra_metadata or {}
153 tmp = tempfile.TemporaryDirectory()
154 saved_cwd = os.getcwd()
156 file_paths = [os.path.join(saved_cwd, file_path) for file_path in files]
157 file_names = [os.path.split(f)[-1] for f in files]
159 if metadata:
160 metadata_content = parse_metadata(metadata.read())
161 else:
162 metadata_content = {}
163 for key, value in extra_metadata.items():
164 if value:
165 metadata_content[key] = value
167 try:
168 os.chdir(tmp.name)
170 if metadata_content:
171 open("metadata.json", "w").write(json.dumps(metadata_content, indent=2))
173 open("runtime.txt", "w").write("python-3.8.3")
175 if branch:
176 install = [
177 "https://github.com/simonw/datasette/archive/{branch}.zip".format(
178 branch=branch
179 )
180 ] + list(install)
181 else:
182 install = ["datasette"] + list(install)
184 open("requirements.txt", "w").write("\n".join(install))
185 os.mkdir("bin")
186 open("bin/post_compile", "w").write(
187 "datasette inspect --inspect-file inspect-data.json"
188 )
190 extras = []
191 if template_dir:
192 link_or_copy_directory(
193 os.path.join(saved_cwd, template_dir),
194 os.path.join(tmp.name, "templates"),
195 )
196 extras.extend(["--template-dir", "templates/"])
197 if plugins_dir:
198 link_or_copy_directory(
199 os.path.join(saved_cwd, plugins_dir), os.path.join(tmp.name, "plugins")
200 )
201 extras.extend(["--plugins-dir", "plugins/"])
202 if version_note:
203 extras.extend(["--version-note", version_note])
204 if metadata_content:
205 extras.extend(["--metadata", "metadata.json"])
206 if extra_options:
207 extras.extend(extra_options.split())
208 for mount_point, path in static:
209 link_or_copy_directory(
210 os.path.join(saved_cwd, path), os.path.join(tmp.name, mount_point)
211 )
212 extras.extend(["--static", "{}:{}".format(mount_point, mount_point)])
214 quoted_files = " ".join(
215 ["-i {}".format(shlex.quote(file_name)) for file_name in file_names]
216 )
217 procfile_cmd = "web: datasette serve --host 0.0.0.0 {quoted_files} --cors --port $PORT --inspect-file inspect-data.json {extras}".format(
218 quoted_files=quoted_files, extras=" ".join(extras)
219 )
220 open("Procfile", "w").write(procfile_cmd)
222 for path, filename in zip(file_paths, file_names):
223 link_or_copy(path, os.path.join(tmp.name, filename))
225 yield
227 finally:
228 tmp.cleanup()
229 os.chdir(saved_cwd)