File size: 44,563 Bytes
461c45d
1
["// Copyright 2020 CIS Maxwell, LLC. All rights reserved.\n// Copyright 2020 The Calyx Institute\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"math\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar input string\n\nvar executable, _ = os.Executable()\nvar cwd = filepath.Dir(executable)\n\nvar adb *exec.Cmd\nvar fastboot *exec.Cmd\n\nvar platformToolsVersion = \"30.0.4\"\nvar platformToolsZip string\n\nvar deviceFactoryFolderMap map[string]string\n\n// Set via LDFLAGS, check Makefile\nvar version string\n\nconst OS = runtime.GOOS\n\nconst (\n\tUDEV_RULES = \"# Google\\nSUBSYSTEM==\\\"usb\\\", ATTR{idVendor}==\\\"18d1\\\", GROUP=\\\"sudo\\\"\\n# Xiaomi\\nSUBSYSTEM==\\\"usb\\\", ATTR{idVendor}==\\\"2717\\\", GROUP=\\\"sudo\\\"\\n\"\n\tRULES_FILE = \"98-device-flasher.rules\"\n\tRULES_PATH = \"/etc/udev/rules.d/\"\n)\n\nvar (\n\tError = Red\n\tWarn  = Yellow\n)\n\nvar (\n\tBlue   = Color(\"\\033[1;34m%s\\033[0m\")\n\tRed    = Color(\"\\033[1;31m%s\\033[0m\")\n\tYellow = Color(\"\\033[1;33m%s\\033[0m\")\n)\n\nfunc Color(color string) func(...interface{}) string {\n\treturn func(args ...interface{}) string {\n\t\treturn fmt.Sprintf(color,\n\t\t\tfmt.Sprint(args...))\n\t}\n}\n\nfunc errorln(err interface{}, fatal bool) {\n\tlog, _ := os.OpenFile(\"error.log\", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)\n\t_, _ = fmt.Fprintln(log, err)\n\t_, _ = fmt.Fprintln(os.Stderr, Error(err))\n\tlog.Close()\n\tif fatal {\n\t\tcleanup()\n\t\tfmt.Println(\"Press enter to exit.\")\n\t\t_, _ = fmt.Scanln(&input)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc warnln(warning interface{}) {\n\tfmt.Println(Warn(warning))\n}\n\nfunc cleanup() {\n\tif OS == \"linux\" {\n\t\t_, err := os.Stat(RULES_PATH + RULES_FILE)\n\t\tif !os.IsNotExist(err) {\n\t\t\t_ = exec.Command(\"sudo\", \"rm\", RULES_PATH+RULES_FILE).Run()\n\t\t}\n\t}\n}\n\nfunc main() {\n\tdefer cleanup()\n\t_ = os.Remove(\"error.log\")\n\tfmt.Println(\"Android Factory Image Flasher version \" + version)\n\t// Map device codenames to their corresponding extracted factory image folders\n\tdeviceFactoryFolderMap = getFactoryFolders()\n\tif len(deviceFactoryFolderMap) < 1 {\n\t\terrorln(errors.New(\"Cannot continue without a device factory image. Exiting...\"), true)\n\t}\n\terr := getPlatformTools()\n\tif err != nil {\n\t\terrorln(\"Cannot continue without Android platform tools. Exiting...\", false)\n\t\terrorln(err, true)\n\t}\n\tif OS == \"linux\" {\n\t\t// Linux weirdness\n\t\tcheckUdevRules()\n\t}\n\tplatformToolCommand := *adb\n\tplatformToolCommand.Args = append(adb.Args, \"start-server\")\n\terr = platformToolCommand.Run()\n\tif err != nil {\n\t\terrorln(\"Cannot start ADB server\", false)\n\t\terrorln(err, true)\n\t}\n\twarnln(\"1. Connect to a wifi network and ensure that no SIM cards are installed\")\n\twarnln(\"2. Enable Developer Options on device (Settings -> About Phone -> tap \\\"Build number\\\" 7 times)\")\n\twarnln(\"3. Enable USB debugging on device (Settings -> System -> Advanced -> Developer Options) and allow the computer to debug (hit \\\"OK\\\" on the popup when USB is connected)\")\n\twarnln(\"4. Enable OEM Unlocking (in the same Developer Options menu)\")\n\tfmt.Println()\n\tfmt.Print(Warn(\"Press ENTER to continue\"))\n\t_, _ = fmt.Scanln(&input)\n\tfmt.Println()\n\t// Map serial numbers to device codenames by extracting them from adb and fastboot command output\n\tdevices := getDevices()\n\tif len(devices) == 0 {\n\t\terrorln(errors.New(\"No devices to be flashed. Exiting...\"), true)\n\t} else if !PARALLEL && len(devices) > 1 {\n\t\terrorln(errors.New(\"More than one device detected. Exiting...\"), true)\n\t}\n\tfmt.Println()\n\tfmt.Println(\"Devices to be flashed: \")\n\tfor serialNumber, device := range devices {\n\t\tfmt.Println(device + \" \" + serialNumber)\n\t}\n\tfmt.Println()\n\tfmt.Print(Warn(\"Press ENTER to continue\"))\n\t_, _ = fmt.Scanln(&input)\n\t// Sequence: unlock bootloader -> execute flash-all script -> relock bootloader\n\tflashDevices(devices)\n}\n\nfunc getFactoryFolders() map[string]string {\n\tfiles, err := ioutil.ReadDir(cwd)\n\tif err != nil {\n\t\terrorln(err, true)\n\t}\n\tdeviceFactoryFolderMap := map[string]string{}\n\tfor _, file := range files {\n\t\tfile := file.Name()\n\t\tif strings.Contains(file, \"factory\") && strings.HasSuffix(file, \".zip\") {\n\t\t\tif strings.HasPrefix(file, \"jasmine\") {\n\t\t\t\tplatformToolsVersion = \"29.0.6\"\n\t\t\t}\n\t\t\textracted, err := extractZip(path.Base(file), cwd)\n\t\t\tif err != nil {\n\t\t\t\terrorln(\"Cannot continue without a factory image. Exiting...\", false)\n\t\t\t\terrorln(err, true)\n\t\t\t}\n\t\t\tdevice := strings.Split(file, \"-\")[0]\n\t\t\tif _, exists := deviceFactoryFolderMap[device]; !exists {\n\t\t\t\tdeviceFactoryFolderMap[device] = extracted[0]\n\t\t\t} else {\n\t\t\t\terrorln(\"More than one factory image available for \"+device, true)\n\t\t\t}\n\t\t}\n\t}\n\treturn deviceFactoryFolderMap\n}\n\nfunc getPlatformTools() error {\n\tplaformToolsUrlMap := map[[2]string]string{\n\t\t[2]string{\"darwin\", \"29.0.6\"}:  \"https://dl.google.com/android/repository/platform-tools_r29.0.6-darwin.zip\",\n\t\t[2]string{\"linux\", \"29.0.6\"}:   \"https://dl.google.com/android/repository/platform-tools_r29.0.6-linux.zip\",\n\t\t[2]string{\"windows\", \"29.0.6\"}: \"https://dl.google.com/android/repository/platform-tools_r29.0.6-windows.zip\",\n\t\t[2]string{\"darwin\", \"30.0.4\"}:  \"https://dl.google.com/android/repository/fbad467867e935dce68a0296b00e6d1e76f15b15.platform-tools_r30.0.4-darwin.zip\",\n\t\t[2]string{\"linux\", \"30.0.4\"}:   \"https://dl.google.com/android/repository/platform-tools_r30.0.4-linux.zip\",\n\t\t[2]string{\"windows\", \"30.0.4\"}: \"https://dl.google.com/android/repository/platform-tools_r30.0.4-windows.zip\",\n\t}\n\tplatformToolsChecksumMap := map[[2]string]string{\n\t\t[2]string{\"darwin\", \"29.0.6\"}:  \"PI:FP:7555e8e24958cae4cfd197135950359b9fe8373d4862a03677f089d215119a3aEND_PI\",\n\t\t[2]string{\"linux\", \"29.0.6\"}:   \"cc9e9d0224d1a917bad71fe12d209dfffe9ce43395e048ab2f07dcfc21101d44\",\n\t\t[2]string{\"windows\", \"29.0.6\"}: \"247210e3c12453545f8e1f76e55de3559c03f2d785487b2e4ac00fe9698a039c\",\n\t\t[2]string{\"darwin\", \"30.0.4\"}:  \"e0db2bdc784c41847f854d6608e91597ebc3cef66686f647125f5a046068a890\",\n\t\t[2]string{\"linux\", \"30.0.4\"}:   \"5be24ed897c7e061ba800bfa7b9ebb4b0f8958cc062f4b2202701e02f2725891\",\n\t\t[2]string{\"windows\", \"30.0.4\"}: \"413182fff6c5957911e231b9e97e6be4fc6a539035e3dfb580b5c54bd5950fee\",\n\t}\n\tplatformToolsOsVersion := [2]string{OS, platformToolsVersion}\n\t_, err := os.Stat(path.Base(plaformToolsUrlMap[platformToolsOsVersion]))\n\tif err != nil {\n\t\terr = downloadFile(plaformToolsUrlMap[platformToolsOsVersion])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tplatformToolsZip = path.Base(plaformToolsUrlMap[platformToolsOsVersion])\n\terr = verifyZip(platformToolsZip, platformToolsChecksumMap[platformToolsOsVersion])\n\tif err != nil {\n\t\tfmt.Println(platformToolsZip + \" checksum verification failed\")\n\t\treturn err\n\t}\n\tplatformToolsPath := cwd + string(os.PathSeparator) + \"platform-tools\" + string(os.PathSeparator)\n\tpathEnvironmentVariable := func() string {\n\t\tif OS == \"windows\" {\n\t\t\treturn \"Path\"\n\t\t} else {\n\t\t\treturn \"PATH\"\n\t\t}\n\t}()\n\t_ = os.Setenv(pathEnvironmentVariable, platformToolsPath+string(os.PathListSeparator)+os.Getenv(pathEnvironmentVariable))\n\tadbPath := platformToolsPath + \"adb\"\n\tfastbootPath := platformToolsPath + \"fastboot\"\n\tif OS == \"windows\" {\n\t\tadbPath += \".exe\"\n\t\tfastbootPath += \".exe\"\n\t}\n\tadb = exec.Command(adbPath)\n\tfastboot = exec.Command(fastbootPath)\n\t// Ensure that no platform tools are running before attempting to overwrite them\n\tkillPlatformTools()\n\t_, err = extractZip(platformToolsZip, cwd)\n\treturn err\n}\n\nfunc checkUdevRules() {\n\t_, err := os.Stat(RULES_PATH)\n\tif os.IsNotExist(err) {\n\t\terr = exec.Command(\"sudo\", \"mkdir\", RULES_PATH).Run()\n\t\tif err != nil {\n\t\t\terrorln(\"Cannot continue without udev rules. Exiting...\", false)\n\t\t\terrorln(err, true)\n\t\t}\n\t}\n\t_, err = os.Stat(RULES_FILE)\n\tif os.IsNotExist(err) {\n\t\terr = ioutil.WriteFile(RULES_FILE, []byte(UDEV_RULES), 0644)\n\t\tif err != nil {\n\t\t\terrorln(\"Cannot continue without udev rules. Exiting...\", false)\n\t\t\terrorln(err, true)\n\t\t}\n\t\terr = exec.Command(\"sudo\", \"cp\", RULES_FILE, RULES_PATH).Run()\n\t\tif err != nil {\n\t\t\terrorln(\"Cannot continue without udev rules. Exiting...\", false)\n\t\t\terrorln(err, true)\n\t\t}\n\t\t_ = exec.Command(\"sudo\", \"udevadm\", \"control\", \"--reload-rules\").Run()\n\t\t_ = exec.Command(\"sudo\", \"udevadm\", \"trigger\").Run()\n\t}\n}\n\nfunc getDevices() map[string]string {\n\tdevices := map[string]string{}\n\tfor _, platformToolCommand := range []exec.Cmd{*adb, *fastboot} {\n\t\tplatformToolCommand.Args = append(platformToolCommand.Args, \"devices\")\n\t\toutput, _ := platformToolCommand.Output()\n\t\tlines := strings.Split(string(output), \"\\n\")\n\t\tif platformToolCommand.Path == adb.Path {\n\t\t\tlines = lines[1:]\n\t\t}\n\t\tfor i, device := range lines {\n\t\t\tif lines[i] != \"\" && lines[i] != \"\\r\" {\n\t\t\t\tserialNumber := strings.Split(device, \"\\t\")[0]\n\t\t\t\tif platformToolCommand.Path == adb.Path {\n\t\t\t\t\tdevice = getProp(\"ro.product.device\", serialNumber)\n\t\t\t\t} else if platformToolCommand.Path == fastboot.Path {\n\t\t\t\t\tdevice = getVar(\"product\", serialNumber)\n\t\t\t\t\tif device == \"jasmine\" {\n\t\t\t\t\t\tdevice += \"_sprout\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfmt.Print(\"Detected \" + device + \" \" + serialNumber)\n\t\t\t\tif _, ok := deviceFactoryFolderMap[device]; ok {\n\t\t\t\t\tdevices[serialNumber] = device\n\t\t\t\t\tfmt.Println()\n\t\t\t\t} else {\n\t\t\t\t\tfmt.Println(\". \" + \"No matching factory image found\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn devices\n}\n\nfunc getVar(prop string, device string) string {\n\tplatformToolCommand := *fastboot\n\tplatformToolCommand.Args = append(fastboot.Args, \"-s\", device, \"getvar\", prop)\n\tout, err := platformToolCommand.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tlines := strings.Split(string(out), \"\\n\")\n\tfor _, line := range lines {\n\t\tif strings.Contains(line, prop) {\n\t\t\treturn strings.Trim(strings.Split(line, \" \")[1], \"\\r\")\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc getProp(prop string, device string) string {\n\tplatformToolCommand := *adb\n\tplatformToolCommand.Args = append(adb.Args, \"-s\", device, \"shell\", \"getprop\", prop)\n\tout, err := platformToolCommand.Output()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.Trim(string(out), \"[]\\n\\r\")\n}\n\nfunc flashDevices(devices map[string]string) {\n\tvar wg sync.WaitGroup\n\tfor serialNumber, device := range devices {\n\t\twg.Add(1)\n\t\tgo func(serialNumber, device string) {\n\t\t\tdefer wg.Done()\n\t\t\tplatformToolCommand := *adb\n\t\t\tplatformToolCommand.Args = append(platformToolCommand.Args, \"-s\", serialNumber, \"reboot\", \"bootloader\")\n\t\t\t_ = platformToolCommand.Run()\n\t\t\tfmt.Println(\"Unlocking \" + device + \" \" + serialNumber + \" bootloader...\")\n\t\t\twarnln(\"5. Please use the volume and power keys on the device to unlock the bootloader\")\n\t\t\tif device == \"jasmine\" || device == \"walleye\" {\n\t\t\t\tfmt.Println()\n\t\t\t\twarnln(\"  5a. Once \" + device + \" \" + serialNumber + \" boots, disconnect its cable and power it off\")\n\t\t\t\twarnln(\"  5b. Then, press volume down + power to boot it into fastboot mode, and connect the cable again.\")\n\t\t\t\tfmt.Println(\"The installation will resume automatically\")\n\t\t\t}\n\t\t\tfor i := 0; getVar(\"unlocked\", serialNumber) != \"yes\"; i++ {\n\t\t\t\tplatformToolCommand = *fastboot\n\t\t\t\tplatformToolCommand.Args = append(platformToolCommand.Args, \"-s\", serialNumber, \"flashing\", \"unlock\")\n\t\t\t\t_ = platformToolCommand.Start()\n\t\t\t\ttime.Sleep(30 * time.Second)\n\t\t\t\tif i >= 2 {\n\t\t\t\t\terrorln(\"Failed to unlock \"+device+\" \"+serialNumber+\" bootloader\", false)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tfmt.Println(\"Flashing \" + device + \" \" + serialNumber + \" bootloader...\")\n\t\t\tflashAll := exec.Command(\".\" + string(os.PathSeparator) + \"flash-all\" + func() string {\n\t\t\t\tif OS == \"windows\" {\n\t\t\t\t\treturn \".bat\"\n\t\t\t\t} else {\n\t\t\t\t\treturn \".sh\"\n\t\t\t\t}\n\t\t\t}())\n\t\t\tflashAll.Dir = deviceFactoryFolderMap[device]\n\t\t\tflashAll.Stderr = os.Stderr\n\t\t\terr := flashAll.Run()\n\t\t\tif err != nil {\n\t\t\t\terrorln(\"Failed to flash \"+device+\" \"+serialNumber, false)\n\t\t\t\terrorln(err.Error(), false)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfmt.Println(\"Locking \" + device + \" \" + serialNumber + \" bootloader...\")\n\t\t\twarnln(\"6. Please use the volume and power keys on the device to lock the bootloader\")\n\t\t\tif device == \"jasmine\" || device == \"walleye\" {\n\t\t\t\tfmt.Println()\n\t\t\t\twarnln(\"  6a. Once \" + device + \" \" + serialNumber + \" boots, disconnect its cable and power it off\")\n\t\t\t\twarnln(\"  6b. Then, press volume down + power to boot it into fastboot mode, and connect the cable again.\")\n\t\t\t\tfmt.Println(\"The installation will resume automatically\")\n\t\t\t}\n\t\t\tfor i := 0; getVar(\"unlocked\", serialNumber) != \"no\"; i++ {\n\t\t\t\tplatformToolCommand = *fastboot\n\t\t\t\tplatformToolCommand.Args = append(platformToolCommand.Args, \"-s\", serialNumber, \"flashing\", \"lock\")\n\t\t\t\t_ = platformToolCommand.Start()\n\t\t\t\ttime.Sleep(30 * time.Second)\n\t\t\t\tif i >= 2 {\n\t\t\t\t\terrorln(\"Failed to lock \"+device+\" \"+serialNumber+\" bootloader\", false)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tfmt.Println(\"Rebooting \" + device + \" \" + serialNumber + \"...\")\n\t\t\tplatformToolCommand = *fastboot\n\t\t\tplatformToolCommand.Args = append(platformToolCommand.Args, \"-s\", serialNumber, \"reboot\")\n\t\t\t_ = platformToolCommand.Start()\n\t\t\twarnln(\"7. Disable OEM unlocking from Developer Options after setting up your device\")\n\t\t}(serialNumber, device)\n\t}\n\twg.Wait()\n\tfmt.Println()\n\tfmt.Println(Blue(\"Flashing complete\"))\n}\n\nfunc killPlatformTools() {\n\t_, err := os.Stat(adb.Path)\n\tif err == nil {\n\t\tplatformToolCommand := *adb\n\t\tplatformToolCommand.Args = append(platformToolCommand.Args, \"kill-server\")\n\t\t_ = platformToolCommand.Run()\n\t}\n\tif OS == \"windows\" {\n\t\t_ = exec.Command(\"taskkill\", \"/IM\", \"fastboot.exe\", \"/F\").Run()\n\t}\n}\n\nfunc downloadFile(url string) error {\n\tfmt.Println(\"Downloading \" + url)\n\tresp, err := http.Get(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tout, err := os.Create(path.Base(url))\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer out.Close()\n\n\tcounter := &WriteCounter{}\n\t_, err = io.Copy(out, io.TeeReader(resp.Body, counter))\n\tfmt.Println()\n\treturn err\n}\n\nfunc extractZip(src string, destination string) ([]string, error) {\n\tfmt.Println(\"Extracting \" + src)\n\tvar filenames []string\n\tr, err := zip.OpenReader(src)\n\tif err != nil {\n\t\treturn filenames, err\n\t}\n\tdefer r.Close()\n\n\tfor _, f := range r.File {\n\t\tfpath := filepath.Join(destination, f.Name)\n\t\tif !strings.HasPrefix(fpath, filepath.Clean(destination)+string(os.PathSeparator)) {\n\t\t\treturn filenames, fmt.Errorf(\"%s is an illegal filepath\", fpath)\n\t\t}\n\t\tfilenames = append(filenames, fpath)\n\t\tif f.FileInfo().IsDir() {\n\t\t\tos.MkdirAll(fpath, os.ModePerm)\n\t\t\tcontinue\n\t\t}\n\t\tif err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {\n\t\t\treturn filenames, err\n\t\t}\n\t\toutFile, err := os.OpenFile(fpath,\n\t\t\tos.O_WRONLY|os.O_CREATE|os.O_TRUNC,\n\t\t\tf.Mode())\n\t\tif err != nil {\n\t\t\treturn filenames, err\n\t\t}\n\t\trc, err := f.Open()\n\t\tif err != nil {\n\t\t\treturn filenames, err\n\t\t}\n\t\t_, err = io.Copy(outFile, rc)\n\t\toutFile.Close()\n\t\trc.Close()\n\t\tif err != nil {\n\t\t\treturn filenames, err\n\t\t}\n\t}\n\treturn filenames, nil\n}\n\nfunc verifyZip(zipfile, sha256sum string) error {\n\tfmt.Println(\"Verifying \" + zipfile)\n\tf, err := os.Open(zipfile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\th := sha256.New()\n\tif _, err := io.Copy(h, f); err != nil {\n\t\treturn err\n\t}\n\tsum := hex.EncodeToString(h.Sum(nil))\n\tif sha256sum == sum {\n\t\treturn nil\n\t}\n\treturn errors.New(\"sha256sum mismatch\")\n}\n\ntype WriteCounter struct {\n\tTotal uint64\n}\n\nfunc (wc *WriteCounter) Write(p []byte) (int, error) {\n\tn := len(p)\n\twc.Total += uint64(n)\n\twc.PrintProgress()\n\treturn n, nil\n}\n\nfunc (wc WriteCounter) PrintProgress() {\n\tfmt.Printf(\"\\r%s\", strings.Repeat(\" \", 35))\n\tfmt.Printf(\"\\rDownloading... %s downloaded\", Bytes(wc.Total))\n}\n\nfunc logn(n, b float64) float64 {\n\treturn math.Log(n) / math.Log(b)\n}\n\nfunc humanateBytes(s uint64, base float64, sizes []string) string {\n\tif s < 10 {\n\t\treturn fmt.Sprintf(\"%d B\", s)\n\t}\n\te := math.Floor(logn(float64(s), base))\n\tsuffix := sizes[int(e)]\n\tval := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10\n\tf := \"%.0f %s\"\n\tif val < 10 {\n\t\tf = \"%.1f %s\"\n\t}\n\n\treturn fmt.Sprintf(f, val, suffix)\n}\n\nfunc Bytes(s uint64) string {\n\tsizes := []string{\"B\", \"kB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\"}\n\treturn humanateBytes(s, 1000, sizes)\n}\n", "# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.\n\"\"\"Centralized catalog of paths.\"\"\"\n\nimport os\n\n\nclass DatasetCatalog(object):\n    DATA_DIR = \"./datasets\"\n    DATASETS = {\n        \"coco_2017_train\": {\n            \"img_dir\": \"coco/train2017\",\n            \"ann_file\": \"coco/annotations/instances_train2017.json\"\n        },\n        \"coco_2017_val\": {\n            \"img_dir\": \"coco/val2017\",\n            \"ann_file\": \"coco/annotations/instances_val2017.json\"\n        },\n        \"coco_2017_test_dev\": {\n            \"img_dir\": \"coco/test2017\",\n            \"ann_file\": \"coco/annotations/image_info_test-dev2017.json\"\n        },\n        \"coco_2014_train\": {\n            \"img_dir\": \"coco/train2014\",\n            \"ann_file\": \"coco/annotations/instances_train2014.json\"\n        },\n        \"coco_2014_val\": {\n            \"img_dir\": \"coco/val2014\",\n            \"ann_file\": \"coco/annotations/instances_val2014.json\"\n        },\n        \"coco_2014_minival\": {\n            \"img_dir\": \"coco/val2014\",\n            \"ann_file\": \"coco/annotations/instances_minival2014.json\"\n        },\n        \"coco_2014_valminusminival\": {\n            \"img_dir\": \"coco/val2014\",\n            \"ann_file\": \"coco/annotations/instances_valminusminival2014.json\"\n        },\n        \"keypoints_coco_2014_train\": {\n            \"img_dir\": \"coco/train2014\",\n            \"ann_file\": \"coco/annotations/person_keypoints_train2014.json\",\n        },\n        \"keypoints_coco_2014_val\": {\n            \"img_dir\": \"coco/val2014\",\n            \"ann_file\": \"coco/annotations/person_keypoints_val2014.json\"\n        },\n        \"keypoints_coco_2014_minival\": {\n            \"img_dir\": \"coco/val2014\",\n            \"ann_file\": \"coco/annotations/person_keypoints_minival2014.json\",\n        },\n        \"keypoints_coco_2014_valminusminival\": {\n            \"img_dir\": \"coco/val2014\",\n            \"ann_file\": \"coco/annotations/person_keypoints_valminusminival2014.json\",\n        },\n        \"voc_2007_train\": {\n            \"data_dir\": \"voc/VOC2007\",\n            \"split\": \"train\"\n        },\n        \"voc_2007_train_cocostyle\": {\n            \"img_dir\": \"voc/VOC2007/JPEGImages\",\n            \"ann_file\": \"voc/VOC2007/Annotations/pascal_train2007.json\"\n        },\n        \"voc_2007_val\": {\n            \"data_dir\": \"voc/VOC2007\",\n            \"split\": \"val\"\n        },\n        \"voc_2007_val_cocostyle\": {\n            \"img_dir\": \"voc/VOC2007/JPEGImages\",\n            \"ann_file\": \"voc/VOC2007/Annotations/pascal_val2007.json\"\n        },\n        \"voc_2007_test\": {\n            \"data_dir\": \"voc/VOC2007\",\n            \"split\": \"test\"\n        },\n        \"voc_2007_test_cocostyle\": {\n            \"img_dir\": \"voc/VOC2007/JPEGImages\",\n            \"ann_file\": \"voc/VOC2007/Annotations/pascal_test2007.json\"\n        },\n        \"voc_2012_train\": {\n            \"data_dir\": \"voc/VOC2012\",\n            \"split\": \"train\"\n        },\n        \"voc_2012_train_cocostyle\": {\n            \"img_dir\": \"voc/VOC2012/JPEGImages\",\n            \"ann_file\": \"voc/VOC2012/Annotations/pascal_train2012.json\"\n        },\n        \"voc_2012_val\": {\n            \"data_dir\": \"voc/VOC2012\",\n            \"split\": \"val\"\n        },\n        \"voc_2012_val_cocostyle\": {\n            \"img_dir\": \"voc/VOC2012/JPEGImages\",\n            \"ann_file\": \"voc/VOC2012/Annotations/pascal_val2012.json\"\n        },\n        \"voc_2012_test\": {\n            \"data_dir\": \"voc/VOC2012\",\n            \"split\": \"test\"\n            # PASCAL VOC2012 doesn't made the test annotations available, so there's no json annotation\n        },\n        \"cityscapes_fine_instanceonly_seg_train_cocostyle\": {\n            \"img_dir\": \"cityscapes/images\",\n            \"ann_file\": \"cityscapes/annotations/instancesonly_filtered_gtFine_train.json\"\n        },\n        \"cityscapes_fine_instanceonly_seg_val_cocostyle\": {\n            \"img_dir\": \"cityscapes/images\",\n            \"ann_file\": \"cityscapes/annotations/instancesonly_filtered_gtFine_val.json\"\n        },\n        \"cityscapes_fine_instanceonly_seg_test_cocostyle\": {\n            \"img_dir\": \"cityscapes/images\",\n            \"ann_file\": \"cityscapes/annotations/instancesonly_filtered_gtFine_test.json\"\n        }\n    }\n\n    @staticmethod\n    def get(name):\n        if \"coco\" in name:\n            data_dir = DatasetCatalog.DATA_DIR\n            attrs = DatasetCatalog.DATASETS[name]\n            args = dict(\n                root=os.path.join(data_dir, attrs[\"img_dir\"]),\n                ann_file=os.path.join(data_dir, attrs[\"ann_file\"]),\n            )\n            return dict(\n                factory=\"COCODataset\",\n                args=args,\n            )\n        elif \"voc\" in name:\n            data_dir = DatasetCatalog.DATA_DIR\n            attrs = DatasetCatalog.DATASETS[name]\n            args = dict(\n                data_dir=os.path.join(data_dir, attrs[\"data_dir\"]),\n                split=attrs[\"split\"],\n            )\n            return dict(\n                factory=\"PascalVOCDataset\",\n                args=args,\n            )\n        raise RuntimeError(\"Dataset not available: {}\".format(name))\n\n\nclass ModelCatalog(object):\n    S3_C2_DETECTRON_URL = \"https://dl.fbaipublicfiles.com/detectron\"\n    C2_IMAGENET_MODELS = {\n        \"MSRA/R-50\": \"ImageNetPretrained/MSRA/R-50.pkl\",\n        \"MSRA/R-50-GN\": \"ImageNetPretrained/47261647/R-50-GN.pkl\",\n        \"MSRA/R-101\": \"ImageNetPretrained/MSRA/R-101.pkl\",\n        \"MSRA/R-101-GN\": \"ImageNetPretrained/47592356/R-101-GN.pkl\",\n        \"FAIR/20171220/X-101-32x8d\": \"ImageNetPretrained/20171220/X-101-32x8d.pkl\",\n        \"FAIR/20171220/X-101-64x4d\": \"ImageNetPretrained/20171220/X-101-64x4d.pkl\",\n    }\n\n    C2_DETECTRON_SUFFIX = \"output/train/{}coco_2014_train%3A{}coco_2014_valminusminival/generalized_rcnn/model_final.pkl\"\n    C2_DETECTRON_MODELS = {\n        \"35857197/e2e_faster_rcnn_R-50-C4_1x\": \"01_33_49.iAX0mXvW\",\n        \"35857345/e2e_faster_rcnn_R-50-FPN_1x\": \"01_36_30.cUF7QR7I\",\n        \"35857890/e2e_faster_rcnn_R-101-FPN_1x\": \"01_38_50.sNxI7sX7\",\n        \"36761737/e2e_faster_rcnn_X-101-32x8d-FPN_1x\": \"06_31_39.5MIHi1fZ\",\n        \"35858791/e2e_mask_rcnn_R-50-C4_1x\": \"01_45_57.ZgkA7hPB\",\n        \"35858933/e2e_mask_rcnn_R-50-FPN_1x\": \"01_48_14.DzEQe4wC\",\n        \"35861795/e2e_mask_rcnn_R-101-FPN_1x\": \"02_31_37.KqyEK4tT\",\n        \"36761843/e2e_mask_rcnn_X-101-32x8d-FPN_1x\": \"06_35_59.RZotkLKI\",\n        \"37129812/e2e_mask_rcnn_X-152-32x8d-FPN-IN5k_1.44x\": \"09_35_36.8pzTQKYK\",\n        # keypoints\n        \"PI:FP:37697547/e2e_keypoint_rcnn_R-50-FPN_1xEND_PI\": \"08_42_54.kdzV35ao\"\n    }\n\n    @staticmethod\n    def get(name):\n        if name.startswith(\"Caffe2Detectron/COCO\"):\n            return ModelCatalog.get_c2_detectron_12_2017_baselines(name)\n        if name.startswith(\"ImageNetPretrained\"):\n            return ModelCatalog.get_c2_imagenet_pretrained(name)\n        raise RuntimeError(\"model not present in the catalog {}\".format(name))\n\n    @staticmethod\n    def get_c2_imagenet_pretrained(name):\n        prefix = ModelCatalog.S3_C2_DETECTRON_URL\n        name = name[len(\"ImageNetPretrained/\"):]\n        name = ModelCatalog.C2_IMAGENET_MODELS[name]\n        url = \"/\".join([prefix, name])\n        return url\n\n    @staticmethod\n    def get_c2_detectron_12_2017_baselines(name):\n        # Detectron C2 models are stored following the structure\n        # prefix/<model_id>/2012_2017_baselines/<model_name>.yaml.<signature>/suffix\n        # we use as identifiers in the catalog Caffe2Detectron/COCO/<model_id>/<model_name>\n        prefix = ModelCatalog.S3_C2_DETECTRON_URL\n        dataset_tag = \"keypoints_\" if \"keypoint\" in name else \"\"\n        suffix = ModelCatalog.C2_DETECTRON_SUFFIX.format(dataset_tag, dataset_tag)\n        # remove identification prefix\n        name = name[len(\"Caffe2Detectron/COCO/\"):]\n        # split in <model_id> and <model_name>\n        model_id, model_name = name.split(\"/\")\n        # parsing to make it match the url address from the Caffe2 models\n        model_name = \"{}.yaml\".format(model_name)\n        signature = ModelCatalog.C2_DETECTRON_MODELS[name]\n        unique_name = \".\".join([model_name, signature])\n        url = \"/\".join([prefix, model_id, \"12_2017_baselines\", unique_name, suffix])\n        return url\n", "# coding: utf-8\n#\n# Copyright 2014 The Oppia Authors. All Rights Reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, softwar\n# distributed under the License is distributed on an \"AS-IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom extensions.rich_text_components import base\n\n\nNONNEGATIVE_INT_SCHEMA = {\n    'type': 'int',\n    'validators': [{\n        'id': 'is_at_least',\n        'min_value': 0\n    }],\n}\n\n\nclass Video(base.BaseRichTextComponent):\n    \"\"\"A rich-text component representing a YouTube video.\"\"\"\n\n    name = 'Video'\n    category = 'Basic Input'\n    description = 'A YouTube video.'\n    frontend_name = 'video'\n    tooltip = 'Insert video'\n\n    _customization_arg_specs = [{\n        'name': 'video_id',\n        'description': (\n            'The YouTube id for this video. This is the 11-character string '\n            'after \\'v=\\' in the video URL.'),\n        'schema': {\n            'type': 'unicode',\n        },\n        'default_value': '',\n    }, {\n        'name': 'start',\n        'description': (\n            'Video start time in seconds: (leave at 0 to start at the '\n            'beginning.)'),\n        'schema': NONNEGATIVE_INT_SCHEMA,\n        'default_value': 0\n    }, {\n        'name': 'end',\n        'description': (\n            'Video end time in seconds: (leave at 0 to play until the end.)'),\n        'schema': NONNEGATIVE_INT_SCHEMA,\n        'default_value': 0\n    }, {\n        'name': 'autoplay',\n        'description': (\n            'Autoplay this video once the question has loaded?'),\n        'schema': {\n            'type': 'bool'\n        },\n        'default_value': False,\n    }]\n\n    icon_data_url = (\n        'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAA'\n        'ABGdBTUEAAK/INwWK6QAAABl0RVh0%0AU29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZ'\n        'TwAAAIfSURBVDjLpZNPaBNBGMXfbrubzBqbg4kL%0A0lJLgiVKE/AP6Kl6UUFQNAeDIAj'\n        'VS08aELx59GQPAREV/4BeiqcqROpRD4pUNCJSS21OgloISWME%0AZ/aPb6ARdNeTCz92m'\n        'O%2B9N9/w7RphGOJ/nsH%2Bolqtvg%2BCYJR8q9VquThxuVz%2BoJTKeZ63Uq/XC38E%0'\n        'A0Jj3ff8%2BOVupVGLbolkzQw5HOqAxQU4wXWWnZrykmYD0QsgAOJe9hpEUcPr8i0GaJ8'\n        'n2vs/sL2h8%0AR66TpVfWTdETHWE6GRGKjGiiKNLii5BSLpN7pBHpgMYhMkm8tPUWz3sL'\n        '2D1wFaY/jvnWcTTaE5Dy%0AjMfTT5J0XIAiTRYn3ASwZ1MKbTmN7z%2BKaHUOYqmb1fcP'\n        'iNa4kQBuyvWAHYfcHGzDgYcx9NKrwJYH%0ACAyF21JiPWBnXMAQOea6bmn%2B4ueYGZi8'\n        'gtymNVobF7BG5prNpjd%2BeW6X4BSUD0gOdCpzA8MpA/v2%0Av15kl4%2BpK0emwHSbjJ'\n        'GBlz%2BvYM1fQeDrYOBTdzOGvDf6EFNr%2BLYjHbBgsaCLxr%2BmoNQjU2vYhRXp%0AgI'\n        'PI:FP:UOmSWWnsJRfjlOZhrexgtYDZ/gWbetNRbNs6QT10GJglNk64HMaGgbAkoMo5fiFNy7CKDEND_PI'\n        'QUGqE%0A5r38YktxAfSqW7Zt33l66WtkAkACjuNsaLVaDxlw5HdJ/86aYrG4WCgUZD6fX'\n        '%2Bjv/U0ymfxoWVZo%0AmuZyf%2B8XqfGP49CCrBUAAAAASUVORK5CYII%3D%0A'\n    )\n", "#\n# Copyright 2014 Google Inc. All rights reserved.\n#\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n# use this file except in compliance with the License. You may obtain a copy of\n# the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n# License for the specific language governing permissions and limitations under\n# the License.\n#\n\n\n\"\"\"Tests for client module.\"\"\"\n\nimport responses\nimport time\n\nimport googlemaps\nfrom googlemaps import client as _client\nimport test as _test\nimport requests\n\nclass ClientTest(_test.TestCase):\n\n    def test_no_api_key(self):\n        with self.assertRaises(Exception):\n            client = googlemaps.Client()\n            client.directions(\"Sydney\", \"Melbourne\")\n\n    def test_invalid_api_key(self):\n        with self.assertRaises(Exception):\n            client = googlemaps.Client(key=\"Invalid key.\")\n            client.directions(\"Sydney\", \"Melbourne\")\n\n    def test_urlencode(self):\n        # See GH #72.\n        encoded_params = _client.urlencode_params([(\"address\", \"=Sydney ~\")])\n        self.assertEqual(\"address=%3DSydney+~\", encoded_params)\n\n    @responses.activate\n    def test_queries_per_second(self):\n        # This test assumes that the time to run a mocked query is\n        # relatively small, eg a few milliseconds. We define a rate of\n        # 3 queries per second, and run double that, which should take at\n        # least 1 second but no more than 2.\n        queries_per_second = 3\n        query_range = range(queries_per_second * 2)\n        for _ in query_range:\n            responses.add(responses.GET,\n                          \"https://maps.googleapis.com/maps/api/geocode/json\",\n                          body='{\"status\":\"OK\",\"results\":[]}',\n                          status=200,\n                          content_type=\"application/json\")\n        client = googlemaps.Client(key=\"AIzaasdf\",\n                                   queries_per_second=queries_per_second)\n        start = time.time()\n        for _ in query_range:\n            client.geocode(\"Sesame St.\")\n        end = time.time()\n        self.assertTrue(start + 1 < end < start + 2)\n\n    @responses.activate\n    def test_key_sent(self):\n        responses.add(responses.GET,\n                      \"https://maps.googleapis.com/maps/api/geocode/json\",\n                      body='{\"status\":\"OK\",\"results\":[]}',\n                      status=200,\n                      content_type=\"application/json\")\n\n        client = googlemaps.Client(key=\"AIzaasdf\")\n        client.geocode(\"Sesame St.\")\n\n        self.assertEqual(1, len(responses.calls))\n        self.assertURLEqual(\"https://maps.googleapis.com/maps/api/geocode/json?\"\n                            \"key=AIzaasdf&address=Sesame+St.\",\n                            responses.calls[0].request.url)\n\n    @responses.activate\n    def test_extra_params(self):\n        responses.add(responses.GET,\n                      \"https://maps.googleapis.com/maps/api/geocode/json\",\n                      body='{\"status\":\"OK\",\"results\":[]}',\n                      status=200,\n                      content_type=\"application/json\")\n\n        client = googlemaps.Client(key=\"AIzaasdf\")\n        client.geocode(\"Sesame St.\", extra_params={\"foo\": \"bar\"})\n\n        self.assertEqual(1, len(responses.calls))\n        self.assertURLEqual(\"https://maps.googleapis.com/maps/api/geocode/json?\"\n                            \"key=AIzaasdf&address=Sesame+St.&foo=bar\",\n                            responses.calls[0].request.url)\n\n    def test_hmac(self):\n        \"\"\"\n        From http://en.wikipedia.org/wiki/Hash-based_message_authentication_code\n\n        HMAC_SHA1(\"key\", \"The quick brown fox jumps over the lazy dog\")\n           = 0xde7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9\n        \"\"\"\n\n        message = \"The quick brown fox jumps over the lazy dog\"\n        key = \"a2V5\" # \"key\" -> base64\n        signature = \"3nybhbi3iqa8ino29wqQcBydtNk=\"\n\n        self.assertEqual(signature, _client.sign_hmac(key, message))\n\n    @responses.activate\n    def test_url_signed(self):\n        responses.add(responses.GET,\n                      \"https://maps.googleapis.com/maps/api/geocode/json\",\n                      body='{\"status\":\"OK\",\"results\":[]}',\n                      status=200,\n                      content_type=\"application/json\")\n\n        client = googlemaps.Client(client_id=\"foo\", client_secret=\"a2V5\")\n        client.geocode(\"Sesame St.\")\n\n        self.assertEqual(1, len(responses.calls))\n\n        # Check ordering of parameters.\n        self.assertIn(\"address=Sesame+St.&client=foo&signature\",\n                responses.calls[0].request.url)\n        self.assertURLEqual(\"https://maps.googleapis.com/maps/api/geocode/json?\"\n                            \"address=Sesame+St.&client=foo&\"\n                            \"PI:FP:signature=fxbWUIcNPZSekVOhp2ul9LW5TpY=END_PI\",\n                            responses.calls[0].request.url)\n\n    @responses.activate\n    def test_ua_sent(self):\n        responses.add(responses.GET,\n                      \"https://maps.googleapis.com/maps/api/geocode/json\",\n                      body='{\"status\":\"OK\",\"results\":[]}',\n                      status=200,\n                      content_type=\"application/json\")\n\n        client = googlemaps.Client(key=\"AIzaasdf\")\n        client.geocode(\"Sesame St.\")\n\n        self.assertEqual(1, len(responses.calls))\n        user_agent = responses.calls[0].request.headers[\"User-Agent\"]\n        self.assertTrue(user_agent.startswith(\"GoogleGeoApiClientPython\"))\n\n    @responses.activate\n    def test_retry(self):\n        class request_callback:\n            def __init__(self):\n                self.first_req = True\n\n            def __call__(self, req):\n                if self.first_req:\n                    self.first_req = False\n                    return (200, {}, '{\"status\":\"OVER_QUERY_LIMIT\"}')\n                return (200, {}, '{\"status\":\"OK\",\"results\":[]}')\n\n        responses.add_callback(responses.GET,\n                \"https://maps.googleapis.com/maps/api/geocode/json\",\n                content_type='application/json',\n                callback=request_callback())\n\n        client = googlemaps.Client(key=\"AIzaasdf\")\n        client.geocode(\"Sesame St.\")\n\n        self.assertEqual(2, len(responses.calls))\n        self.assertEqual(responses.calls[0].request.url, responses.calls[1].request.url)\n\n    @responses.activate\n    def test_transport_error(self):\n        responses.add(responses.GET,\n                      \"https://maps.googleapis.com/maps/api/geocode/json\",\n                      status=404,\n                      content_type='application/json')\n\n        client = googlemaps.Client(key=\"AIzaasdf\")\n        with self.assertRaises(googlemaps.exceptions.HTTPError) as e:\n            client.geocode(\"Foo\")\n\n        self.assertEqual(e.exception.status_code, 404)\n\n    @responses.activate\n    def test_host_override(self):\n        responses.add(responses.GET,\n                      \"https://foo.com/bar\",\n                      body='{\"status\":\"OK\",\"results\":[]}',\n                      status=200,\n                      content_type=\"application/json\")\n\n        client = googlemaps.Client(key=\"AIzaasdf\")\n        client._get(\"/bar\", {}, base_url=\"https://foo.com\")\n\n        self.assertEqual(1, len(responses.calls))\n\n    @responses.activate\n    def test_custom_extract(self):\n        def custom_extract(resp):\n            return resp.json()\n\n        responses.add(responses.GET,\n                      \"https://maps.googleapis.com/bar\",\n                      body='{\"error\":\"errormessage\"}',\n                      status=403,\n                      content_type=\"application/json\")\n\n        client = googlemaps.Client(key=\"AIzaasdf\")\n        b = client._get(\"/bar\", {}, extract_body=custom_extract)\n        self.assertEqual(1, len(responses.calls))\n        self.assertEqual(\"errormessage\", b[\"error\"])\n\n    @responses.activate\n    def test_retry_intermittent(self):\n        class request_callback:\n            def __init__(self):\n                self.first_req = True\n\n            def __call__(self, req):\n                if self.first_req:\n                    self.first_req = False\n                    return (500, {}, 'Internal Server Error.')\n                return (200, {}, '{\"status\":\"OK\",\"results\":[]}')\n\n        responses.add_callback(responses.GET,\n                \"https://maps.googleapis.com/maps/api/geocode/json\",\n                content_type=\"application/json\",\n                callback=request_callback())\n\n        client = googlemaps.Client(key=\"AIzaasdf\")\n        client.geocode(\"Sesame St.\")\n\n        self.assertEqual(2, len(responses.calls))\n\n    def test_channel_without_client_id(self):\n        with self.assertRaises(ValueError):\n            client = googlemaps.Client(key=\"AIzaasdf\", channel=\"mychannel\")\n\n    def test_invalid_channel(self):\n        # Cf. limitations here:\n        # https://developers.google.com/maps/premium/reports\n        # /usage-reports#channels\n        with self.assertRaises(ValueError):\n            client = googlemaps.Client(client_id=\"foo\", client_secret=\"a2V5\",\n                                       channel=\"auieauie$? \")\n\n    def test_auth_url_with_channel(self):\n        client = googlemaps.Client(key=\"AIzaasdf\",\n                                   client_id=\"foo\",\n                                   client_secret=\"a2V5\",\n                                   channel=\"MyChannel_1\")\n\n        # Check ordering of parameters + signature.\n        auth_url = client._generate_auth_url(\"/test\",\n                                             {\"param\": \"param\"},\n                                             accepts_clientid=True)\n        self.assertEqual(auth_url, \"/test?param=param\"\n                            \"&channel=MyChannel_1\"\n                            \"&client=foo\"\n                            \"&signature=OH18GuQto_mEpxj99UimKskvo4k=\")\n\n        # Check if added to requests to API with accepts_clientid=False\n        auth_url = client._generate_auth_url(\"/test\",\n                                             {\"param\": \"param\"},\n                                             accepts_clientid=False)\n        self.assertEqual(auth_url, \"/test?param=param&key=AIzaasdf\")\n\n    def test_requests_version(self):\n        client_args_timeout = {\n            \"key\": \"AIzaasdf\",\n            \"client_id\": \"foo\",\n            \"client_secret\": \"a2V5\",\n            \"channel\": \"MyChannel_1\",\n            \"connect_timeout\": 5,\n            \"read_timeout\": 5\n        }\n        client_args = client_args_timeout.copy()\n        del client_args[\"connect_timeout\"]\n        del client_args[\"read_timeout\"]\n\n        requests.__version__ = '2.3.0'\n        with self.assertRaises(NotImplementedError):\n            googlemaps.Client(**client_args_timeout)\n        googlemaps.Client(**client_args)\n\n        requests.__version__ = '2.4.0'\n        googlemaps.Client(**client_args_timeout)\n        googlemaps.Client(**client_args)\n\n    @responses.activate\n    def test_no_retry_over_query_limit(self):\n        responses.add(responses.GET,\n                      \"https://maps.googleapis.com/foo\",\n                      body='{\"status\":\"OVER_QUERY_LIMIT\"}',\n                      status=200,\n                      content_type=\"application/json\")\n\n        client = googlemaps.Client(key=\"AIzaasdf\",\n                                   retry_over_query_limit=False)\n\n        with self.assertRaises(googlemaps.exceptions.ApiError):\n            client._request(\"/foo\", {})\n\n        self.assertEqual(1, len(responses.calls))\n", "require 'spec_helper'\nrequire 'yt/models/playlist_item'\n\ndescribe Yt::PlaylistItem, :device_app do\n  subject(:item) { Yt::PlaylistItem.new id: id, auth: $account }\n\n  context 'given an existing playlist item' do\n    let(:id) { 'PI:FP:PLjW_GNR5Ir0GMlbJzA-aW0UV8TchJFb8p3uzrLNcZKPYEND_PI' }\n\n    it 'returns valid metadata' do\n      expect(item.title).to be_a String\n      expect(item.description).to be_a String\n      expect(item.thumbnail_url).to be_a String\n      expect(item.published_at).to be_a Time\n      expect(item.channel_id).to be_a String\n      expect(item.channel_title).to be_a String\n      expect(item.playlist_id).to be_a String\n      expect(item.position).to be_an Integer\n      expect(item.video_id).to be_a String\n      expect(item.video).to be_a Yt::Video\n      expect(item.privacy_status).to be_a String\n    end\n  end\n\n  context 'given an unknown playlist item' do\n    let(:id) { 'not-a-playlist-item-id' }\n\n    it { expect{item.snippet}.to raise_error Yt::Errors::RequestError }\n  end\n\n\n  context 'given one of my own playlist items that I want to update' do\n    before(:all) do\n      @my_playlist = $account.create_playlist title: \"Yt Test Update Playlist Item #{rand}\"\n      @my_playlist.add_video 'MESycYJytkU'\n      @my_playlist_item = @my_playlist.add_video 'MESycYJytkU'\n    end\n    after(:all) { @my_playlist.delete }\n\n    let(:id) { @my_playlist_item.id }\n    let!(:old_title) { @my_playlist_item.title }\n    let!(:old_privacy_status) { @my_playlist_item.privacy_status }\n    let(:update) { @my_playlist_item.update attrs }\n\n    context 'given I update the position' do\n      let(:attrs) { {position: 0} }\n\n      specify 'only updates the position' do\n        expect(update).to be true\n        expect(@my_playlist_item.position).to be 0\n        expect(@my_playlist_item.title).to eq old_title\n        expect(@my_playlist_item.privacy_status).to eq old_privacy_status\n      end\n    end\n  end\nend"]