annotate scripts/hvresources/uploader.py @ 269:ac8eb07afcf5

Oxygen text added to each render.cpp file for the default projects. Text includes project explanation from Wiki, edited in places. Empty project added as a default project. Doxyfile updated. Each of the project locations added to INPUT configuration option. Consider just watching the whole project file so all new projects are automatically pulled through.
author Robert Jack <robert.h.jack@gmail.com>
date Tue, 17 May 2016 15:40:16 +0100
parents c768ed1055b0
children df710a88e355 83e1acf38d35
rev   line source
giuliomoro@229 1 # Copyright 2015,2016 Enzien Audio, Ltd. All Rights Reserved.
chris@160 2
chris@160 3 import argparse
chris@160 4 import getpass
chris@160 5 import json
chris@160 6 import os
chris@160 7 import requests
chris@160 8 import shutil
chris@160 9 import stat
chris@160 10 import tempfile
chris@160 11 import time
chris@160 12 import urlparse
chris@160 13 import zipfile
chris@160 14
chris@160 15 class Colours:
chris@160 16 purple = "\033[95m"
chris@160 17 cyan = "\033[96m"
chris@160 18 dark_cyan = "\033[36m"
chris@160 19 blue = "\033[94m"
chris@160 20 green = "\033[92m"
chris@160 21 yellow = "\033[93m"
chris@160 22 red = "\033[91m"
chris@160 23 bold = "\033[1m"
chris@160 24 underline = "\033[4m"
chris@160 25 end = "\033[0m"
chris@160 26
chris@160 27 def __zip_dir(in_dir, zip_path, file_filter=None):
chris@160 28 zf = zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED)
chris@160 29 for subdir, dirs, files in os.walk(in_dir):
chris@160 30 for file in files:
chris@160 31 if (file_filter is None) or (len(file_filter) > 0 and file.lower().split(".")[-1] in file_filter):
chris@160 32 zf.write(
chris@160 33 filename=os.path.join(subdir,file),
chris@160 34 arcname=os.path.relpath(os.path.join(subdir,file), start=in_dir))
chris@160 35 return zip_path
chris@160 36
chris@160 37 def __unzip(zip_path, target_dir):
chris@160 38 """Unzip a file to a given directory. All destination files are overwritten.
chris@160 39 """
chris@160 40 zipfile.ZipFile(zip_path).extractall(target_dir)
chris@160 41
chris@160 42 def main():
chris@160 43 parser = argparse.ArgumentParser(
chris@160 44 description="Compiles a Pure Data file.")
chris@160 45 parser.add_argument(
chris@160 46 "input_dir",
giuliomoro@229 47 help="A directory containing _main.pd. All .pd files in the directory structure will be uploaded.")
chris@160 48 parser.add_argument(
chris@160 49 "-n", "--name",
chris@160 50 default="heavy",
giuliomoro@229 51 help="Patch name. If it doesn't exist on the Heavy site, the uploader will fail.")
chris@160 52 parser.add_argument(
chris@160 53 "-g", "--gen",
chris@160 54 nargs="+",
chris@160 55 default=["c"],
giuliomoro@229 56 help="List of generator outputs. Currently supported generators are "
giuliomoro@229 57 "'c', 'js', 'pdext', 'pdext-osx', 'unity', 'unity-osx', "
giuliomoro@229 58 "'unity-win-x86', 'unity-win-x86_64', 'wwise', 'wwise-win-x86_64', "
giuliomoro@229 59 "'vst2' ,'vst2-osx', and 'vst2-win-x86_64'.")
chris@160 60 parser.add_argument(
chris@160 61 "-b",
giuliomoro@229 62 help="All files will be placed in the output directory, placed in their own subdirectory corresponding to the generator name.",
giuliomoro@229 63 action="count")
giuliomoro@229 64 parser.add_argument(
giuliomoro@229 65 "-y",
giuliomoro@229 66 help="Extract only the generated C files. Static files are deleted. "
giuliomoro@229 67 "Only effective for the 'c' generator.",
chris@160 68 action="count")
chris@160 69 parser.add_argument(
chris@160 70 "-o", "--out",
chris@160 71 nargs="+",
chris@160 72 default=["./"], # by default
chris@160 73 help="List of destination directories for retrieved files. Order should be the same as for --gen.")
chris@160 74 parser.add_argument(
chris@160 75 "-d", "--domain",
chris@160 76 default="https://enzienaudio.com",
chris@160 77 help="Domain. Default is https://enzienaudio.com.")
chris@160 78 parser.add_argument(
chris@160 79 "-x",
chris@160 80 help="Don't save the returned token.",
chris@160 81 action="count")
chris@160 82 parser.add_argument(
chris@160 83 "-z",
chris@160 84 help="Force the use of a password, regardless of saved token.",
chris@160 85 action="count")
chris@160 86 parser.add_argument(
chris@160 87 "--noverify",
giuliomoro@229 88 help="Don't verify the SSL connection. This is generally a very bad idea.",
chris@160 89 action="count")
chris@160 90 parser.add_argument(
chris@160 91 "-v", "--verbose",
chris@160 92 help="Show debugging information.",
chris@160 93 action="count")
giuliomoro@229 94 parser.add_argument(
giuliomoro@229 95 "-t", "--token",
giuliomoro@229 96 help="Use the specified token.",
giuliomoro@229 97 )
chris@160 98 args = parser.parse_args()
chris@160 99
chris@160 100 domain = args.domain or "https://enzienaudio.com"
chris@160 101
chris@160 102 post_data = {}
chris@160 103
chris@160 104 # token should be stored in ~/.heavy/token
chris@160 105 token_path = os.path.expanduser(os.path.join("~/", ".heavy", "token"))
giuliomoro@229 106
giuliomoro@229 107 if args.token is not None:
giuliomoro@229 108 # check if token has been passed as a command line arg...
giuliomoro@229 109 post_data["credentials"] = {"token": args.token}
giuliomoro@229 110 elif os.path.exists(token_path) and not args.z:
giuliomoro@229 111 # ...or if it is stored in the user's home directory
chris@160 112 with open(token_path, "r") as f:
giuliomoro@229 113 post_data["credentials"] = {"token": f.read()}
chris@160 114 else:
chris@160 115 # otherwise, get the username and password
chris@160 116 post_data["credentials"] = {
chris@160 117 "username": raw_input("Enter username: "),
chris@160 118 "password": getpass.getpass("Enter password: ")
chris@160 119 }
chris@160 120
chris@160 121 tick = time.time()
chris@160 122
chris@160 123 # make a temporary directory
chris@160 124 temp_dir = tempfile.mkdtemp(prefix="lroyal-")
chris@160 125
chris@160 126 # zip up the pd directory into the temporary directory
chris@160 127 try:
chris@160 128 if not os.path.exists(os.path.join(args.input_dir, "_main.pd")):
chris@160 129 raise Exception("Root Pd directory does not contain a file named _main.pd.")
chris@160 130 zip_path = __zip_dir(
chris@160 131 args.input_dir,
chris@160 132 os.path.join(temp_dir, "archive.zip"),
chris@160 133 file_filter={"pd"})
chris@160 134 except Exception as e:
chris@160 135 print e
chris@160 136 shutil.rmtree(temp_dir) # clean up the temporary directory
chris@160 137 return
chris@160 138
chris@160 139 post_data["name"] = args.name
chris@160 140
chris@160 141 # the outputs to generate (always include c)
giuliomoro@229 142 __SUPPORTED_GENERATOR_SET = {
giuliomoro@229 143 "c", "js",
giuliomoro@229 144 "pdext", "pdext-osx",
giuliomoro@229 145 "unity", "unity-osx", "unity-win-x86", "unity-win-x86_64",
giuliomoro@229 146 "wwise", "wwise-win-x86_64",
giuliomoro@229 147 "vst2", "vst2-osx", "vst2-win-x86_64",
giuliomoro@229 148 }
chris@160 149 post_data["gen"] = list(({"c"} | set(args.gen)) & __SUPPORTED_GENERATOR_SET)
chris@160 150
chris@160 151 # upload the job, get the response back
chris@160 152 # NOTE(mhroth): multipart-encoded file can only be sent as a flat dictionary,
chris@160 153 # but we want to send a json encoded deep dictionary. So we do a bit of a hack.
chris@160 154 r = requests.post(
chris@160 155 urlparse.urljoin(domain, "/a/heavy"),
chris@160 156 data={"json":json.dumps(post_data)},
chris@160 157 files={"file": (os.path.basename(zip_path), open(zip_path, "rb"), "application/zip")},
chris@160 158 verify=False if args.noverify else True)
chris@160 159
chris@160 160 if r.status_code != requests.codes.ok:
chris@160 161 shutil.rmtree(temp_dir) # clean up the temporary directory
giuliomoro@229 162 print "Getting a weird error? Get the latest uploader at https://enzienaudio.com/static/uploader.py"
chris@160 163 r.raise_for_status() # raise an exception
chris@160 164
chris@160 165 # decode the JSON API response
chris@160 166 r_json = r.json()
chris@160 167
chris@160 168 """
chris@160 169 {
chris@160 170 "data": {
chris@160 171 "compileTime": 0.05078411102294922,
chris@160 172 "id": "mhroth/asdf/Edp2G",
chris@160 173 "slug": "Edp2G",
chris@160 174 "index": 3,
chris@160 175 "links": {
chris@160 176 "files": {
chris@160 177 "linkage": [
chris@160 178 {
chris@160 179 "id": "mhroth/asdf/Edp2G/c",
chris@160 180 "type": "file"
chris@160 181 }
chris@160 182 ],
chris@160 183 "self": "https://enzienaudio.com/h/mhroth/asdf/Edp2G/files"
chris@160 184 },
chris@160 185 "project": {
chris@160 186 "linkage": {
chris@160 187 "id": "mhroth/asdf",
chris@160 188 "type": "project"
chris@160 189 },
chris@160 190 "self": "https://enzienaudio.com/h/mhroth/asdf"
chris@160 191 },
chris@160 192 "self": "https://enzienaudio.com/h/mhroth/asdf/Edp2G",
chris@160 193 "user": {
chris@160 194 "linkage": {
chris@160 195 "id": "mhroth",
chris@160 196 "type": "user"
chris@160 197 },
chris@160 198 "self": "https://enzienaudio.com/h/mhroth"
chris@160 199 }
chris@160 200 },
chris@160 201 "type": "job"
chris@160 202 },
chris@160 203 "included": [
chris@160 204 {
chris@160 205 "filename": "file.c.zip",
chris@160 206 "generator": "c",
chris@160 207 "id": "mhroth/asdf/Edp2G/c",
chris@160 208 "links": {
chris@160 209 "self": "https://enzienaudio.com/h/mhroth/asdf/Edp2G/c/file.c.zip"
chris@160 210 },
chris@160 211 "mime": "application/zip",
chris@160 212 "type": "file"
chris@160 213 }
chris@160 214 ],
giuliomoro@229 215 "warnings": [
giuliomoro@229 216 {"details": "blah blah blah"}
giuliomoro@229 217 ],
chris@160 218 "meta": {
chris@160 219 "token": "11AS0qPRmjTUHEMSovPEvzjodnzB1xaz"
chris@160 220 }
chris@160 221 }
chris@160 222 """
chris@160 223 reply_json = r.json()
chris@160 224 if args.verbose:
chris@160 225 print json.dumps(
chris@160 226 reply_json,
chris@160 227 sort_keys=True,
chris@160 228 indent=2,
chris@160 229 separators=(",", ": "))
chris@160 230
chris@160 231 # update the api token, if present
chris@160 232 if "token" in reply_json.get("meta",{}) and not args.x:
giuliomoro@229 233 if args.token is not None:
giuliomoro@229 234 if reply_json["meta"]["token"] != args.token:
giuliomoro@229 235 print "WARNING: Token returned by API is not the same as the "
giuliomoro@229 236 "token supplied at the command line. (old = %s, new = %s)".format(
giuliomoro@229 237 args.token,
giuliomoro@229 238 reply_json["meta"]["token"])
giuliomoro@229 239 else:
giuliomoro@229 240 if not os.path.exists(os.path.dirname(token_path)):
giuliomoro@229 241 # ensure that the .heavy directory exists
giuliomoro@229 242 os.makedirs(os.path.dirname(token_path))
giuliomoro@229 243 with open(token_path, "w") as f:
giuliomoro@229 244 f.write(reply_json["meta"]["token"])
giuliomoro@229 245 # force rw------- permissions on the file
giuliomoro@229 246 os.chmod(token_path, stat.S_IRUSR | stat.S_IWUSR)
chris@160 247
chris@160 248 # print any warnings
giuliomoro@229 249 for i,x in enumerate(r_json.get("warnings",[])):
giuliomoro@229 250 print "{3}) {0}Warning:{1} {2}".format(
giuliomoro@229 251 Colours.yellow, Colours.end, x["detail"], i+1)
chris@160 252
chris@160 253 # check for errors
chris@160 254 if len(r_json.get("errors",[])) > 0:
chris@160 255 shutil.rmtree(temp_dir) # clean up the temporary directory
giuliomoro@229 256 for i,x in enumerate(r_json["errors"]):
giuliomoro@229 257 print "{3}) {0}Error:{1} {2}".format(
giuliomoro@229 258 Colours.red, Colours.end, x["detail"], i+1)
chris@160 259 return
chris@160 260
chris@160 261 # retrieve all requested files
chris@160 262 for i,g in enumerate(args.gen):
chris@160 263 file_url = __get_file_url_for_generator(reply_json, g)
chris@160 264 if file_url is not None and (len(args.out) > i or args.b):
chris@160 265 r = requests.get(
chris@160 266 file_url,
chris@160 267 cookies={"token": reply_json["meta"]["token"]},
chris@160 268 verify=False if args.noverify else True)
chris@160 269 r.raise_for_status()
chris@160 270
chris@160 271 # write the reply to a temporary file
chris@160 272 c_zip_path = os.path.join(temp_dir, "archive.{0}.zip".format(g))
chris@160 273 with open(c_zip_path, "wb") as f:
chris@160 274 f.write(r.content)
chris@160 275
chris@160 276 # unzip the files to where they belong
chris@160 277 if args.b:
chris@160 278 target_dir = os.path.join(os.path.abspath(os.path.expanduser(args.out[0])), g)
chris@160 279 else:
chris@160 280 target_dir = os.path.abspath(os.path.expanduser(args.out[i]))
chris@160 281 if not os.path.exists(target_dir):
chris@160 282 os.makedirs(target_dir) # ensure that the output directory exists
chris@160 283 __unzip(c_zip_path, target_dir)
chris@160 284
giuliomoro@229 285 if g == "c" and args.y:
giuliomoro@229 286 keep_files = ("_{0}.h".format(args.name), "_{0}.c".format(args.name))
giuliomoro@229 287 for f in os.listdir(target_dir):
giuliomoro@229 288 if not f.endswith(keep_files):
giuliomoro@229 289 os.remove(os.path.join(target_dir, f));
giuliomoro@229 290
chris@160 291 print "{0} files placed in {1}".format(g, target_dir)
chris@160 292 else:
chris@160 293 print "{0}Warning:{1} {2} files could not be retrieved.".format(
chris@160 294 Colours.yellow, Colours.end,
chris@160 295 g)
chris@160 296
chris@160 297 # delete the temporary directory
chris@160 298 shutil.rmtree(temp_dir)
chris@160 299
giuliomoro@229 300 print "Job URL:", reply_json["data"]["links"]["self"]
chris@160 301 print "Total request time: {0}ms".format(int(1000.0*(time.time()-tick)))
giuliomoro@229 302 print "Heavy version:", reply_json["meta"]["version"]
chris@160 303
chris@160 304 def __get_file_url_for_generator(json_api, g):
chris@160 305 """Returns the file link for a specific generator.
chris@160 306 Returns None if no link could be found.
chris@160 307 """
chris@160 308 for i in json_api["included"]:
chris@160 309 if g == i["generator"]:
chris@160 310 return i["links"]["self"]
chris@160 311 return None # by default, return None
chris@160 312
chris@160 313
chris@160 314
chris@160 315 if __name__ == "__main__":
chris@160 316 main()