Hide keyboard shortcuts

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 

9 

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 

15 

16 

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 ) 

51 

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"]) 

65 

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 } 

75 

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} 

92 

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) 

114 

115 for app in apps: 

116 if app["name"] == name: 

117 app_name = name 

118 break 

119 

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

128 

129 for key, value in environment_variables.items(): 

130 call( 

131 ["heroku", "config:set", "-a", app_name, "{}={}".format(key, value)] 

132 ) 

133 

134 call(["heroku", "builds:create", "-a", app_name, "--include-vcs-ignore"]) 

135 

136 

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() 

155 

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] 

158 

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 

166 

167 try: 

168 os.chdir(tmp.name) 

169 

170 if metadata_content: 

171 open("metadata.json", "w").write(json.dumps(metadata_content, indent=2)) 

172 

173 open("runtime.txt", "w").write("python-3.8.3") 

174 

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) 

183 

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 ) 

189 

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)]) 

213 

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) 

221 

222 for path, filename in zip(file_paths, file_names): 

223 link_or_copy(path, os.path.join(tmp.name, filename)) 

224 

225 yield 

226 

227 finally: 

228 tmp.cleanup() 

229 os.chdir(saved_cwd)