diff --git a/blue-green/deploy.sh b/blue-green/deploy.sh new file mode 100755 index 0000000..32820a1 --- /dev/null +++ b/blue-green/deploy.sh @@ -0,0 +1,4 @@ +gcloud run deploy blue-green \ + --source . \ + --gpu 1 \ + --gpu-type nvidia-l4 diff --git a/blue-green/index.js b/blue-green/index.js new file mode 100644 index 0000000..1591195 --- /dev/null +++ b/blue-green/index.js @@ -0,0 +1,20 @@ +const express = require('express'); +const app = express(); + +const port = process.env.PORT || 8080; +app.listen(port, () => { + console.log(`App listening on port ${port}`); +}); + +app.get('/', async (req, res) => { + const html = ` + + +

+ Green +

+ + + `; + res.send(html); +}) diff --git a/blue-green/package-lock.json b/blue-green/package-lock.json new file mode 100644 index 0000000..661a180 --- /dev/null +++ b/blue-green/package-lock.json @@ -0,0 +1,697 @@ +{ + "name": "blue-green", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "blue-green", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^4.19.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/blue-green/package.json b/blue-green/package.json new file mode 100644 index 0000000..8e9b723 --- /dev/null +++ b/blue-green/package.json @@ -0,0 +1,15 @@ +{ + "name": "blue-green", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.19.2" + } +} diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/.dockerignore b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/.dockerignore new file mode 100644 index 0000000..ef36f19 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/.dockerignore @@ -0,0 +1,42 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +*.bak +*.bat +*.swp +notes.txt +save/ +tmp/ +tools/ + +**/classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.next +**/.cache +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/build +**/dist +LICENSE +README.md diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/Dockerfile b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/Dockerfile new file mode 100644 index 0000000..58f0f14 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/Dockerfile @@ -0,0 +1,32 @@ +ARG NODE_VERSION=20.12.0 + +# Builds a 1120 MB image +# FROM node:${NODE_VERSION} + +# Builds a 225 MB image +# FROM node:${NODE_VERSION}-slim + +# Builds a 159 MB image +FROM node:${NODE_VERSION}-alpine + +# Use production node environment by default. +ENV NODE_ENV production + +WORKDIR /app + +RUN chown node:node ./ + +# Run the application as a non-root user. +USER node + +# Copy the rest of the source files into the image. +COPY . . + +# Expose the port that the application listens on. +EXPOSE 8080 + +# RUN npm ci --omit=dev && npm cache clean +RUN npm ci --omit=dev + +# Run the application. +CMD ["node", "./bin/www"] diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/app.js b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/app.js new file mode 100644 index 0000000..c2f7ad9 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/app.js @@ -0,0 +1,141 @@ +var createError = require('http-errors'); +var express = require('express'); +var path = require('path'); +var cookieParser = require('cookie-parser'); +var logger = require('morgan'); +const https = require('https'); +const session = require('cookie-session') +const { doubleCsrf } = require("csrf-csrf"); + +const secrets = require('./secrets'); +const gemini = require('./gemini'); + +// Page Routers +var indexRouter = require('./routes/index'); +var aboutRouter = require('./routes/about'); +var geminiRouter = require('./routes/gemini'); + +const CSRF_SECRET = "super csrf secret"; +const COOKIES_SECRET = "super cookie secret"; +const CSRF_COOKIE_NAME = "x-csrf-token"; // This is referenced in app.js + +var api_key = null; + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +app.use(logger('dev')); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser(COOKIES_SECRET)); +app.use(express.static(path.join(__dirname, 'public'))); + +// app.use('/', indexRouter); +app.use('/about', aboutRouter); +app.use('/gemini', geminiRouter); + +app.set('trust proxy', 1) // trust first proxy +app.use(session({ + secret: COOKIES_SECRET, + resave: false, + saveUninitialized: true, + cookie: { secure: true } +})) + +const { invalidCsrfTokenError, generateToken, doubleCsrfProtection } = + doubleCsrf({ + getSecret: () => CSRF_SECRET, + cookieName: CSRF_COOKIE_NAME, + cookieOptions: { sameSite: false, secure: false, signed: true }, // not ideal for production, development only + }); + +// Error handling, validation error interception +const csrfErrorHandler = (error, req, res, next) => { + if (error == invalidCsrfTokenError) { + res.status(403).json({ + error: "csrf validation error", + }); + } else { + next(); + } +}; + +// app.use(doubleCsrfProtection); + +app.get('/', async (req, res) => { + const csrfToken = generateToken(req, res); + + res.render('index', {csrfToken: csrfToken}); +}); + +app.post( + '/ask', + doubleCsrfProtection, + csrfErrorHandler, + async (req, res) => { + + if (!("text" in req.body)) { + msg = "Error: Form validation failed."; + res.setHeader('Content-type', 'application/json'); + res.end(create_response(msg)); + return; + } + + const token = req.body.token; + const model = req.body.model; + const question = req.body.text; + + if (question.length == 0) { + msg = "Please enter a question."; + res.setHeader('Content-type', 'application/json'); + res.end(create_response(msg)); + return; + } + + if (api_key == null) { + api_key = await secrets.init_secrets(); + } + + if (api_key == null) { + msg = "Error: Secrets Manager access failed"; + res.setHeader('Content-type', 'application/json'); + res.end(create_response(msg)); + return; + } + + try { + answer = await gemini.ask_gemini(api_key, model, question); + } catch (error) { + console.log(`Exception: ${error.message}`); + answer = 'Error: request failed. Try again'; + } + + res.setHeader('Content-type', 'application/json'); + res.end(create_response(answer)); +}) + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +function create_response(msg) { + resp = { 'text': msg} + return JSON.stringify(resp); +} + +module.exports = app; diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/bin/www b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/bin/www new file mode 100644 index 0000000..8b17da9 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('node-express:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '8080'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/gemini.js b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/gemini.js new file mode 100644 index 0000000..819ae3b --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/gemini.js @@ -0,0 +1,55 @@ +const markdownit = require('markdown-it'); + +async function ask_gemini(gemini_api_key, model, question) { + const host = "https://generativelanguage.googleapis.com"; + const path = "/v1beta/models/gemini-pro:generateContent"; + const url = host + path; + + headers = { + "x-goog-api-key": gemini_api_key, + "Content-type": "application/json" + } + + const data = { + "contents": [ + { + "parts":[ + { + "text": question + } + ] + } + ] + }; + + var response = await fetch(url, { + method: "POST", + body: JSON.stringify(data), + headers: headers, + }) + .then((response) => response.json()) + .then((json) => { +// console.log(JSON.stringify(json)); + + if (json["candidates"][0]["finishReason"] == 'SAFETY') { + console.log(JSON.stringify(json)); + return 'Gemini refused the question for safety reasons'; + } + + return json["candidates"][0]["content"]["parts"][0]["text"] + }); + + const md = markdownit({ + html: true, + linkify: true, + typographer: true, + }); + + const html = md.render(response); + + return html; +} + +module.exports = { + ask_gemini: ask_gemini +}; diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/package-lock.json b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/package-lock.json new file mode 100644 index 0000000..f34a453 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/package-lock.json @@ -0,0 +1,2001 @@ +{ + "name": "geminiapp", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "geminiapp", + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "@google-cloud/secret-manager": "^5.2.0", + "cookie-parser": "^1.4.6", + "cookie-session": "^2.1.0", + "csrf-csrf": "^3.0.3", + "debug": "^2.6.9", + "ejs": "^3.1.9", + "express": "^4.19.2", + "http-errors": "^1.6.3", + "markdown-it": "^14.1.0", + "morgan": "^1.9.1" + } + }, + "node_modules/@google-cloud/secret-manager": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/secret-manager/-/secret-manager-5.2.0.tgz", + "integrity": "sha512-sGIbXCq20CAI7w8Mh5Rl6yASwkq1K350s+//P/DIdNLBHbTdVcfDYcsS+bSOQwt0JEUXTgn/imIL69VqR8+6gw==", + "dependencies": { + "google-gax": "^4.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.4.tgz", + "integrity": "sha512-MqBisuxTkYvPFnEiu+dag3xG/NBUDzSbAFAWlzfkGnQkjVZ6by3h4atbBc+Ikqup1z5BfB4BN18gKWR1YyppNw==", + "dependencies": { + "@grpc/proto-loader": "^0.7.10", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.12.tgz", + "integrity": "sha512-DCVwMxqYzpUCiDMl7hQ384FqP4T3DbNpXU8pt681l3UWCip1WUiD5JrkImUwCB9a7f2cq4CUTmi5r/xIMRPY1Q==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/node": { + "version": "20.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.2.tgz", + "integrity": "sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-session": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.0.tgz", + "integrity": "sha512-u73BDmR8QLGcs+Lprs0cfbcAPKl2HnPcjpwRXT41sEV4DRJ2+W0vJEEZkG31ofkx+HZflA70siRIjiTdIodmOQ==", + "dependencies": { + "cookies": "0.9.1", + "debug": "3.2.7", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cookie-session/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/cookie-session/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csrf-csrf": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-3.0.3.tgz", + "integrity": "sha512-NxRERyDiWGH/MLw5KNl46FwVX36vwK0ppLJizNPa7K72FE+3T+WbOotjKkR5V4Q9lPZei+RtcCQna1rMLCjDFQ==", + "dependencies": { + "http-errors": "^2.0.0" + } + }, + "node_modules/csrf-csrf/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz", + "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/google-auth-library": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.7.0.tgz", + "integrity": "sha512-I/AvzBiUXDzLOy4iIZ2W+Zq33W4lcukQv1nl7C8WUA6SQwyQwUwu3waNmWNAvzds//FG8SZ+DnKnW/2k6mQS8A==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.2.tgz", + "integrity": "sha512-2mw7qgei2LPdtGrmd1zvxQviOcduTnsvAWYzCxhOWXK4IQKmQztHnDQwD0ApB690fBQJemFKSU7DnceAy3RLzw==", + "dependencies": { + "@grpc/grpc-js": "~1.10.0", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.6", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz", + "integrity": "sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA==", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/package.json b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/package.json new file mode 100644 index 0000000..d4cf341 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/package.json @@ -0,0 +1,28 @@ +{ + "name": "geminiapp", + "version": "0.3.0", + "description": "This application implements the Gemini REST API using an API Key.", + "private": true, + "author": "John J. Hanley", + "license": "MIT", + "keywords": [ + "Google", + "Gemini" + ], + "scripts": { + "start": "node ./bin/www", + "watch": "node --watch ./bin/www" + }, + "dependencies": { + "@google-cloud/secret-manager": "^5.2.0", + "cookie-parser": "^1.4.6", + "cookie-session": "^2.1.0", + "csrf-csrf": "^3.0.3", + "debug": "^2.6.9", + "ejs": "^3.1.9", + "express": "^4.19.2", + "http-errors": "^1.6.3", + "markdown-it": "^14.1.0", + "morgan": "^1.9.1" + } +} diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/public/app.js b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/public/app.js new file mode 100644 index 0000000..baf89fd --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/public/app.js @@ -0,0 +1,80 @@ +// Toggle the Submit button to a Loading... button, or vice versa +function toggleSubmitButton() { + const submitButton = document.querySelector('#input-form button[type=submit]'); + + // Flip the value true->false or false->true + submitButton.disabled = !submitButton.disabled; + + // Flip the button's text back to "Waiting..."" or "Submit" + const submitButtonText = submitButton.querySelector('.submit-button-text'); + if(submitButtonText.innerHTML === 'Waiting...') { + submitButtonText.innerHTML = 'Submit'; + } else { + submitButtonText.innerHTML = 'Waiting...'; + } + + // Show or Hide the loading spinner + const submitButtonSpinner = submitButton.querySelector('.submit-button-spinner') + submitButtonSpinner.hidden = !submitButtonSpinner.hidden; +} + +// Process the user's form input +function processFormInput(form) { + // Get values from the form + const token = form.csrf_token.value.trim(); + const topic = form.topic.value.trim(); + const model = form.model.value.trim(); + + // Update the Submit button to indicate we're done loading + toggleSubmitButton(); + + // Clear the output of any existing content + document.querySelector('#output').innerHTML = ''; + + // Send the question + send(token, topic, model); +} + +function send(token, topic, model) { + fetch("ask", { + method: "POST", + headers: { + 'x-csrf-token': token, + 'Content-type': "application/json" + }, + body: JSON.stringify({ token: token, text: topic, model: model }) + }) + .then(response => { + return response.json(); + }) + .then(data => { + document.querySelector('#output').innerHTML = data["text"]; + toggleSubmitButton(); + }) + .catch(error => { + console.log(error) + toggleSubmitButton(); + }) +} + +function main() { + // Wait for the user to submit the form + document.querySelector('#input-form').onsubmit = function(e) { + // Stop the form from submitting, we'll handle it in the browser with JS + e.preventDefault(); + + // Process the data in the form, passing the form to the function + processFormInput(e.target) + }; + + // Update the character count when the user enters any text in the topic textarea + document.querySelector('#topic').oninput = function(e) { + // Get the current length + const length = e.target.value.length; + // Update the badge text + document.querySelector('#topic-badge').innerText = `${length} characters`; + } +} + +// Wait for the DOM to be ready before we start +addEventListener('DOMContentLoaded', main); diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/public/favicon.ico b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/public/favicon.ico new file mode 100644 index 0000000..98f74ce Binary files /dev/null and b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/public/favicon.ico differ diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/public/stylesheets/style.css b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/public/stylesheets/style.css new file mode 100644 index 0000000..9453385 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/public/stylesheets/style.css @@ -0,0 +1,8 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/routes/about.js b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/routes/about.js new file mode 100644 index 0000000..38894fc --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/routes/about.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET about page. */ +router.get('/', function(req, res, next) { + res.render('about', { title: 'About' }); +}); + +module.exports = router; diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/routes/gemini.js b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/routes/gemini.js new file mode 100644 index 0000000..fc93667 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/routes/gemini.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET gemini page. */ +router.get('/', function(req, res, next) { + res.render('gemini', { title: 'Gemini' }); +}); + +module.exports = router; diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/routes/index.js b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/routes/index.js new file mode 100644 index 0000000..ecca96a --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/routes/index.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +module.exports = router; diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/secrets.js b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/secrets.js new file mode 100644 index 0000000..ece8c6d --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/secrets.js @@ -0,0 +1,25 @@ +const {SecretManagerServiceClient} = require('@google-cloud/secret-manager').v1; + +const PROJECT_ID = "[ENTER_YOUR_PROJECT_ID_HERE]" +const SECRET_NAME = "GEMINI_API_KEY" + +async function init_secrets() { + try { + const name = `projects/${PROJECT_ID}/secrets/${SECRET_NAME}/versions/latest` + const secretmanagerClient = new SecretManagerServiceClient(); + + const request = { + name, + }; + const [response] = await secretmanagerClient.accessSecretVersion(request); + + return response.payload.data; + } catch (error) { + console.log(`Exception: ${error.message}`); + return null; + } +} + +module.exports = { + init_secrets: init_secrets +}; diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/about.ejs b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/about.ejs new file mode 100644 index 0000000..a3f679c --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/about.ejs @@ -0,0 +1,41 @@ + + + + + + Gemini AI + + + + +
+<%- include('./partials/nav'); %> + +

Gemini Application

+ +

+This application implements the Gemini REST API using Node.js 20.12 and Express.js and is deployed on Google Cloud Run. +

+ +

+This application supports sending questions to Google Gemini and displays the response. The purpose is to demonstrate how to interface with Google Gemini. +

+ +

+Three Google Gemini models are supported: +

+

+ +<%- include('./partials/footer'); %> +
+ + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/error.ejs b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/error.ejs new file mode 100644 index 0000000..7cf94ed --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/error.ejs @@ -0,0 +1,3 @@ +

<%= message %>

+

<%= error.status %>

+
<%= error.stack %>
diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/gemini.ejs b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/gemini.ejs new file mode 100644 index 0000000..1ebbd2d --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/gemini.ejs @@ -0,0 +1,52 @@ + + + + + + Gemini AI + + + + +
+<%- include('./partials/nav'); %> + +

Google Gemini

+ +

+Google Gemini is a recently launched family of large language models (LLMs) created by Google DeepMind. It's considered their most capable AI model yet, designed to compete with OpenAI's GPT-4. +

+ +

+Here's a breakdown of what Gemini offers: +

+ + + +
+ +

+Overall, Google Gemini is a significant development in AI, offering a powerful and versatile suite of tools for developers and users alike. +

+ +<%- include('./partials/footer'); %> +
+ + + + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/index.ejs b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/index.ejs new file mode 100644 index 0000000..e8ddc76 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/index.ejs @@ -0,0 +1,116 @@ + + + + + + Gemini AI + + + + + + + + +
+<%- include('./partials/nav'); %> + + +
+
+

What would you like to know?

+

+ This AI model's knowledge cutoff is April 2023. +

+

+ Enter a question for Artificial Intelligence to answer. +

+
+ + +
+
+
+
+ + +
+ + + + +
+
+
+ + +
+ +
+
+ +
+ +
+
+
+
+ <%- include('./partials/footer'); %> +
+
+ + + + + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/partials/footer.ejs b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/partials/footer.ejs new file mode 100644 index 0000000..6f1675f --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/partials/footer.ejs @@ -0,0 +1,7 @@ +
+
+
+
+
+

Version 0.3 - March 31, 2024 - Node.js 20.12 Express

+

© 2024 John Hanley

diff --git a/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/partials/nav.ejs b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/partials/nav.ejs new file mode 100644 index 0000000..2c5aea8 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Node-Express/views/partials/nav.ejs @@ -0,0 +1,21 @@ + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.dockerignore b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.dockerignore new file mode 100644 index 0000000..b0a3e9e --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.dockerignore @@ -0,0 +1,14 @@ +.dockerignore +.gitignore +.pylintrc +Dockerfile +LICENSE +*.bat +*.md +*.sh +**.swp +notes.txt +save/ +tools/ +venv/ +__pycache__ diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.gcloudignore b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.gcloudignore new file mode 100644 index 0000000..008e213 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.gcloudignore @@ -0,0 +1,14 @@ +__pycache__ +.dockerignore +.pylintrc +.gitignore +.gcloudignore +*.bat +*.md +*.sh +*.swp +LICENSE +notes.txt +save/ +venv/ +tools/ diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.pylintrc b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.pylintrc new file mode 100644 index 0000000..50d9b8c --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/.pylintrc @@ -0,0 +1,5 @@ +[MESSAGES CONTROL] +disable=invalid-name, broad-exception-caught + +[FORMAT] +indent-string="\t" diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_DOCKER.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_DOCKER.md new file mode 100644 index 0000000..a41049b --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_DOCKER.md @@ -0,0 +1,28 @@ +### Build and Run locally with Docker +TODO: Publish my Docker container build and run tools. + +TODO: Publish my Docker tools + +### Example command to build the container: + + - **docker build -t gemini-python-django .** + +### Example script to run the container (Docker on Windows): + + - Notice some of the options to use a service account inside the container. On my system, I keep secrets, services accounts, etc in a special directory. This command is setup for development and testing. + +``` +@if not defined GCP_PROJECT_ID ( + @echo Please define the environment variable GCP_PROJECT_ID + Exit /B 1 +) + +docker run -it --rm --name gemini-python-django ^ +-p 8080:8080 ^ +-v %cd%:/work ^ +-v %APPDATA%\gcloud:/root/.config ^ +-v c:/config:/config ^ +-e GOOGLE_APPLICATION_CREDENTIALS=/config/service-account.json ^ +-e GCP_PROJECT_ID=%GCP_PROJECT_ID% ^ +gemini-python-django +``` diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_LINUX.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_LINUX.md new file mode 100644 index 0000000..c909158 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_LINUX.md @@ -0,0 +1,50 @@ +### Testing on Linux + + - This application is tested with Python 3.12. + - From this directory create a Python virtual environment: + + - **python -m venv venv** + - **venv/Scripts/activate.sh** + + - Install dependencies + - **python -m pip install -r requirements.txt** + + - Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project. + - Example: `export GCP_PROJECT_ID=myproject-123456`. + + - Configure Google Cloud Secrets Manager with your Google Gemini API Key. + + - Run the application + - **python manage.py runserver** + + - Launch a web browser and connect to **http://localhost:8080/** + +### Linux Tools for Google Cloud + +The **tools/linux** directory contains shell scripts to build and deploy to Google Cloud Run: + +- **gcp_build.sh** - Builds the container using Google Cloud Build to Google Artifact Registry. +- **gcp_deploy.sh** - Deploys the container from Google Artifact Registry to Google Cloud Run. +- **gcp_check_build_upload.sh** - Output a list of files that will be upload to Google Cloud Buil. Run this command to make sure only required files are uploaded. Runs the command **gcloud meta list-files-for-upload**. + +Review both files and make any desired changes to the region, location, repository, etc. The changes must match in both files. + + REGION=us-central1 + SERVICE_NAME=gemini-python-django-v0 + IMAGE_NAME=gemini-python-django-v0 + LOCATION=us-central1 + REPOSITORY=gemini-project + +### Build and Deploy from Linux or WSL +1. OPTIONAL. From this directory execute `source ./add_tools.sh`. This adds the **tools/linux** directory to the PATH. The alternate is to specify the build tool using the syntax **tools/windows/TOOLNAME**. + + - Example: **./tools/linux/gcp_build.sh** +2. Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project. + + - Example: `export GCP_PROJECT_ID=myproject-123456`. +3. Verify the list of files to be included in the container image: + - gcp_check_build_upload.sh** +4. To build the container execute: + - **gcp_build.sh** +5. To deploy the container to Cloud Run execute: + - **gcp_deploy.sh** diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_WINDOWS.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_WINDOWS.md new file mode 100644 index 0000000..baedd15 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/BUILD_WINDOWS.md @@ -0,0 +1,50 @@ +### Testing on Windows + + - This application is tested with Python 3.12. + - From this directory create a Python virtual environment: + + - **python -m venv venv** + - **venv\Scripts\activate.bat** + + - Install dependencies + - **python -m pip install -r requirements.txt** + + - Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project. + - Example: `set GCP_PROJECT_ID=myproject-123456`. + + - Configure Google Cloud Secrets Manager with your Google Gemini API Key. + + - Run the application + - **python manage.py runserver** + + - Launch a web browser and connect to **http://localhost:8080/** + +### Windows Tools for Google Cloud + +The **tools\windows** directory contains batch files to build and deploy to Google Cloud Run: + +- **gcp_build.bat** - Builds the container using Google Cloud Build to Google Artifact Registry. +- **gcp_deploy.bat** - Deploys the container from Google Artifact Registry to Google Cloud Run. +- **gcp_check_build_upload.bat** - Output a list of files that will be upload to Google Cloud Buil. Run this command to make sure only required files are uploaded. Runs the command **gcloud meta list-files-for-upload**. + +Review both files and make any desired changes to the region, location, repository, etc. The changes must match in both files. + + @set REGION=us-central1 + @set SERVICE_NAME=gemini-python-django-v0 + @set IMAGE_NAME=gemini-python-django-v0 + @set LOCATION=us-central1 + @set REPOSITORY=gemini-project + +### Build and Deploy from Windows +1. OPTIONAL. From this directory execute `add_tools.bat`. This adds the **tools\windows** directory to the PATH. The alternate is to specify the build tool using the syntax **tools\windows\TOOLNAME**. + + - Example: **.\tools\windows\gcp_build.bat** +2. Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project. + + - Example: `set GCP_PROJECT_ID=myproject-123456`. +3. Verify the list of files to be included in the container image: + - gcp_check_build_upload.bat** +4. To build the container execute: + - **gcp_build.bat** +5. To deploy the container to Cloud Run execute: + - **gcp_deploy.bat** diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/Dockerfile b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/Dockerfile new file mode 100644 index 0000000..67f4250 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/Dockerfile @@ -0,0 +1,46 @@ +# Use the official Python 3 image. +# https://hub.docker.com/_/python +# +# python:3 builds a 1060 MB image - 342 MB in Google Container Registry +# FROM python:3 +# +# python:3-slim builds a 172 MB image - 60 MB in Google Container Registry +# FROM python:3-slim +# +# python:3-alpine builds a 97 MB image - 32 MB in Google Container Registry +FROM python:3-alpine + +# RUN apt-get update -y +# RUN apt-get install -y python-pip + +# COPY db.sqlite3 /tmp + +# Create and change to the app directory. +WORKDIR /app + +COPY . . + +# FIX +# RUN chmod 444 requirements.txt + +RUN adduser app -D app + +# Run the application as a non-root user. +USER app + +# Fix warning message: WARNING: The script is installed in '/home/app/.local/bin' which is not on PATH. +ENV PATH=${PATH}:/home/app/.local/bin + +RUN python -m pip install --no-cache-dir -r requirements.txt + +# Service must listen to $PORT environment variable. +# This default value facilitates local development. +ENV PORT 8080 + +# ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Run the web service on container startup. +ENTRYPOINT ["python", "manage.py"] +# CMD ["runserver", "0.0.0.0:8080"] +CMD ["runserver"] diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/LICENSE b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/LICENSE new file mode 100644 index 0000000..f1c7291 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 John J. Hanley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/README.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/README.md new file mode 100644 index 0000000..75192b2 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/README.md @@ -0,0 +1,31 @@ +# Gemini Python Django Application + +This section contains source code for the Python Django application. + + - Python 3.12 + Django + +## Build and Deploy to Google Cloud Run + +### Requirements + +#### Google Cloud CLI + + - [Google Cloud CLI](https://cloud.google.com/cli). Tested with version 470 (2024-04-26). + +#### Google Gemini API Key + + - [Google Gemini API Key](https://aistudio.google.com/app/prompts/new_chat/). + +#### Google Cloud Secrets Manager + + - The application reads the Google Gemini API Key in Google Secrets Manager. The secret name is **GEMINI_API_KEY**. + - TODO: Publish my tool to create and rotate the secret. + +#### Google Cloud Run Permissions + - The service account attached to Google Cloud reads the secret from Google Secrets Manager. Add the role **Secret Manager Secret Accessor** to the project's IAM for the service account. + - TODO: Pubish my tools to modify IAM permissions + +Operating System Specific Instructions: + - [Docker](BUILD_DOCKER.md) + - [Linux](BUILD_LINUX.md) + - [Windows](BUILD_WINDOWS.md) diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.bat new file mode 100644 index 0000000..8cfd8f3 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.bat @@ -0,0 +1,5 @@ +set TOOLS=%cd%\tools\windows + +@set PATH=%TOOLS%;%PATH% + +@echo Added tools to the path diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.sh new file mode 100644 index 0000000..9f5b9fa --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/add_tools.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +TOOLS=`pwd`/tools/linux + +export PATH=$PATH:$TOOLS + +echo Added tools to the path diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/__init__.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/asgi.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/asgi.py new file mode 100644 index 0000000..698e849 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for geminiapp project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geminiapp.settings') + +application = get_asgi_application() diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini.py new file mode 100644 index 0000000..081a25e --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini.py @@ -0,0 +1,144 @@ +''' +This module implements the Google Gemini REST API + https://ai.google.dev/tutorials/rest_quickstart +''' + +import json +import requests + +#------------------------------------------------------------------------------- +# Markdown to HTML converter +#------------------------------------------------------------------------------- + +import markdown + +#------------------------------------------------------------------------------- +# This module implements utilities for the Google Gemini REST API +#------------------------------------------------------------------------------- + +from .gcp_gemini_utils import get_gemini_model_endpoint, get_gemini_response_text + +def ask_gemini(api_key, model, question): + ''' Interact with Google Gemini ''' + + #--------------------------------------------------------------------- + # Google Gemini REST API Documentation + # https://ai.google.dev/api/rest + #--------------------------------------------------------------------- + + #--------------------------------------------------------------------- + # Validate api_key + #--------------------------------------------------------------------- + + if api_key is None or len(api_key) == 0: + return "Internal Error: Missing API Key" + + #--------------------------------------------------------------------- + # Verify question parameter + # + # TODO: Validate question + # Only the string length for zero is checked at this time. + # What is the maximum length supported. Gemini specifies token + # which is about 4 characters. + # https://ai.google.dev/models/gemini + # Gemini 1.0 Pro input token limit is 30,720. + # Gemini 1.5 Pro input token limit is 1,048,576. + #--------------------------------------------------------------------- + + if question is None or len(question) == 0: + return "Please enter a question" + + #--------------------------------------------------------------------- + # Get the Gemini Model to use + #--------------------------------------------------------------------- + + url = get_gemini_model_endpoint(model) + + headers = { + "x-goog-api-key": api_key, + "Content-type": "application/json" + } + + #--------------------------------------------------------------------- + # Format the JSON request + #--------------------------------------------------------------------- + + data = { + "contents": [ + { + "parts":[ + { + "text": question + } + ] + } + ] + } + + #--------------------------------------------------------------------- + # Issue the HTTP POST request + #--------------------------------------------------------------------- + + try: + # print(json.dumps(data, indent=4)) # debug + + response = requests.post(url, headers=headers, json=data, timeout=120) + + # print(response.status_code) # debug + # print(response.content) # debug + + if response.status_code == 404: + print(response.content) # print error message + msg = "**Error: The Gemini model is not available for your project or does not exist**" + return markdown.markdown(msg) + + if response.status_code >= 400: + print(response.content) # print error message + msg = "**Error: Request to Gemini failed**" + return markdown.markdown(msg) + + #--------------------------------------------------------------------- + # Process the output + # TODO: This needs better handling. + # Gemini returns various formats that are not yet documented. + # An important item is to process the key "finishReason". + # Normal requests return "STOP", but I have seen "SAFETY" + # which means the request was rejected. See the link: + # https://ai.google.dev/api/rest/v1/GenerateContentResponse#FinishReason + # + # https://ai.google.dev/api/rest/v1/Content + #--------------------------------------------------------------------- + + resp = response.json() + + # print(json.dumps(resp, indent=4)) # debug + + reason = resp["candidates"][0]["finishReason"] + + # print("Reason:", reason) # debug + + if reason == "STOP": + # Good status + text = get_gemini_response_text(resp) + elif reason == "MAX_TOKENS": + # Error + text = "**Gemini Error: The maximum number of tokens as specified in the request was reached.**" + elif reason == "RECITATION": + # Error + text = "**Gemini Error: The candidate content was flagged for recitation reasons.**" + elif reason == "SAFETY": + # Error + text = "**Gemini Error: The candidate content was flagged for safety reasons.**" + else: + # Error + text = f"**Gemini Error: Gemini refused the question for {reason}**" + + #--------------------------------------------------------------------- + # Gemini returns Markdown, convert to HTML + #--------------------------------------------------------------------- + + html = markdown.markdown(text, extensions=['tables']) + return html.replace('', '
') + except Exception as e: + print(e) # print error message + return None diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini_utils.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini_utils.py new file mode 100644 index 0000000..640a9fc --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_gemini_utils.py @@ -0,0 +1,48 @@ +''' +This module implements utilities for the Google Gemini REST API + https://ai.google.dev/tutorials/rest_quickstart +''' + +def get_gemini_model_endpoint(model): + ''' Determine which Gemini model to use and return the REST URL ''' + + host = "https://generativelanguage.googleapis.com" + + #--------------------------------------------------------------------- + # Validate model + #--------------------------------------------------------------------- + + if model is None or len(model) == 0: + # Default to model (Gemini 1.0 Pro) + model = "gemini_1_0_pro_latest" + + #--------------------------------------------------------------------- + # Select the Gemini LLM model to use + #--------------------------------------------------------------------- + + if model == "gemini_1_0_pro_latest": + path = "/v1beta/models/gemini-1.0-pro-latest:generateContent" + elif model == "gemini_1_0_ultra_latest": + path = "/v1beta/models/gemini-1.0-ultra-latest:generateContent" + elif model == "gemini_1_5_pro_latest": + path = "/v1beta/models/gemini-1.5-pro-latest:generateContent" + else: + # Default model (Gemini 1.0 Pro) + path = "/v1beta/models/gemini-pro:generateContent" + + return host + path + +def get_gemini_response_text(resp): + ''' Process the Gemini response and return the content text ''' + + text = '' + + candidates = resp["candidates"] + + for candidate in candidates: + parts = candidate["content"]["parts"] + + for part in parts: + text += part.get("text", "") + + return text diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_secrets.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_secrets.py new file mode 100644 index 0000000..8fa7872 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_secrets.py @@ -0,0 +1,55 @@ +''' +This module fetches the Gemini API Key stored in Google Secrets Manager + https://cloud.google.com/python/docs/reference/secretmanager/latest +''' + +# Google Secrets Manager imports +from google.cloud import secretmanager_v1 + +def init_secrets(project_id, secret_name): + ''' Fetch the GEMINI_API_KEY stored in Google Secrets Manager ''' + + try: + #--------------------------------------------------------------------- + # Secrets Manager reports an error if byte strings are used + #--------------------------------------------------------------------- + + if isinstance(project_id, bytes): + project_id = project_id.decode('utf-8') + + if isinstance(secret_name, bytes): + secret_name = secret_name.decode('utf-8') + + #--------------------------------------------------------------------- + # Initialize the Secrets Manager Client + #--------------------------------------------------------------------- + + client = secretmanager_v1.SecretManagerServiceClient() + + #--------------------------------------------------------------------- + # Format the secret name + # In app.py, the secret name is set by SECRET_NAME + #--------------------------------------------------------------------- + + name = f"projects/{project_id}/secrets/{secret_name}/versions/latest" + + #--------------------------------------------------------------------- + # Build the client request + #--------------------------------------------------------------------- + + req = secretmanager_v1.AccessSecretVersionRequest( + name=name + ) + + #--------------------------------------------------------------------- + # Fetch the secret + #--------------------------------------------------------------------- + + response = client.access_secret_version(request=req) + + api_key = response.payload.data.decode('utf-8') + + return api_key + except Exception as e: + print(e) # print error message + return None diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_utils.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_utils.py new file mode 100644 index 0000000..e2b39d2 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/gcp_utils.py @@ -0,0 +1,56 @@ +''' +This module implements various Google Cloud Project functions +''' + +import requests + +def get_project_id(): + ''' + This function reads the Google Cloud Project ID from the Metadata service + ''' + + try: + url = "http://metadata.google.internal/computeMetadata/v1/project/project-id" + # url = "http://metadata.goog/v1/project/project-id" + + headers = { + "Metadata-Flavor": "Google" + } + + response = requests.get(url, headers=headers, timeout=0.5) + + if response.status_code >= 400: + print(response.content) # print error message + return None + + return response.content.decode("utf-8") + except Exception as e: + print(e) # print error message + return None + +def get_client_ip(): + ''' + Returns the client IP address. + That IP might be IPv4 or IPv6 depending on how the client connected. + + if "x-forwarded-for" in request.headers: + return request.headers.getlist("x-forwarded-for")[0].rpartition(" ")[-1] + + return request.remote_addr +''' + return 'FIX IP ADDRESS' + +def get_host(): + ''' + Returns the host header + + On Google Cloud Run the HTTP Host header cannot be forged. + 1) The host header is used by the GFE to know which Cloud Run instance to forware to. + 2) The GFE only forwards HTTPS requets. That means the host header must match + one of the managed certificates. + + if "host" in request.headers: + return request.headers.get("host", "localhost") + ''' + + return "localhost" diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/settings.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/settings.py new file mode 100644 index 0000000..1dbe6b2 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/settings.py @@ -0,0 +1,137 @@ +""" +Django settings for geminiapp project. + +Generated by 'django-admin startproject' using Django 5.0.3. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +# Generate key with: +# python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" +SECRET_KEY = '#(j6i_m3zvm9s6lnl$%###_t18nsbaz6)!@y)-37cj$rhx6za5' + +# SECURITY WARNING: don't run with debug turned on in production! +# TODO: Create a method to configure debug +DEBUG = True +DEBUG = False + +# TODO: FIX allowed hosts +ALLOWED_HOSTS = ["*"] + +# TODO: FIX - how to get this to work? I modified manage.py instead +# RUNSERVERPLUS_SERVER_ADDRESS_PORT = '0.0.0.0:8080' + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django_extensions', + 'geminiapp' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'geminiapp.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'geminiapp.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', +# 'NAME': BASE_DIR / 'db.sqlite3', + 'NAME': '/tmp/db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/5.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.0/howto/static-files/ + +STATIC_URL = 'static/' +STATIC_ROOT = './static' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +USE_X_FORWARDED_HOST = True +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/app.js b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/app.js new file mode 100644 index 0000000..6ba069a --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/app.js @@ -0,0 +1,102 @@ +// Toggle the Submit button to a Loading... button, or vice versa +function toggleSubmitButton() { + const submitButton = document.querySelector('#input-form button[type=submit]'); + + // Flip the value true->false or false->true + submitButton.disabled = !submitButton.disabled; + + // Flip the button's text back to "Waiting..."" or "Submit" + const submitButtonText = submitButton.querySelector('.submit-button-text'); + if(submitButtonText.innerHTML === 'Waiting...') { + submitButtonText.innerHTML = 'Submit'; + } else { + submitButtonText.innerHTML = 'Waiting...'; + } + + // Show or Hide the loading spinner + const submitButtonSpinner = submitButton.querySelector('.submit-button-spinner') + submitButtonSpinner.hidden = !submitButtonSpinner.hidden; +} + +// Process the user's form input +function processFormInput(form) { + // Get values from the form + // TODO: FIX CSRF token + // const token = form.csrf_token.value.trim(); + const token = '' + const topic = form.topic.value.trim(); + const model = form.model.value.trim(); + + // Update the Submit button to indicate we're done loading + toggleSubmitButton(); + + // Clear the output of any existing content + document.querySelector('#output').innerHTML = ''; + + // Send the question + send(token, topic, model); +} + +// Source: +// https://django.readthedocs.io/en/latest/howto/csrf.html#including-the-csrf-token-in-an-unprotected-view +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + +function send(token, topic, model) { + const csrftoken = getCookie('csrftoken'); + + fetch("ask", { + method: "POST", + headers: { + 'X-CSRFToken': csrftoken, + 'Content-type': "application/json" + }, + body: JSON.stringify({ token: token, text: topic, model: model }) + }) + .then(response => { + return response.json(); + }) + .then(data => { + document.querySelector('#output').innerHTML = data["text"]; + toggleSubmitButton(); + }) + .catch(error => { + console.log(error) + toggleSubmitButton(); + }) +} + +function main() { + // Wait for the user to submit the form + document.querySelector('#input-form').onsubmit = function(e) { + // Stop the form from submitting, we'll handle it in the browser with JS + e.preventDefault(); + + // Process the data in the form, passing the form to the function + processFormInput(e.target) + }; + + // Update the character count when the user enters any text in the topic textarea + document.querySelector('#topic').oninput = function(e) { + // Get the current length + const length = e.target.value.length; + // Update the badge text + document.querySelector('#topic-badge').innerText = `${length} characters`; + } +} + +// Wait for the DOM to be ready before we start +addEventListener('DOMContentLoaded', main); diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/favicon.ico b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/favicon.ico new file mode 100644 index 0000000..98f74ce Binary files /dev/null and b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/static/favicon.ico differ diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/about.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/about.html new file mode 100644 index 0000000..5fc9fbb --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/about.html @@ -0,0 +1,45 @@ +{% load static %} + + + + + + + Gemini AI + + + + + + +
+{% include 'nav.html' %} + +

Gemini Application

+ +

+This application implements the Gemini REST API using ASP.NET Core 8.0 and is deployed on Google Cloud Run. +

+ +

+This application supports sending questions to Google Gemini and displays the response. The purpose is to demonstrate how to interface with Google Gemini. +

+ +

+Three Google Gemini models are supported: +

+

+ +{% include 'footer.html' %} +
+ + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/footer.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/footer.html new file mode 100644 index 0000000..e1cf920 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/footer.html @@ -0,0 +1,12 @@ +
+
+
+
+
+

+ Client IP: {{client_ip}} +
+ Version 0.3.1 - April 3, 2024 - Python 3.12 Django +
+ © 2024 John Hanley +

diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/gemini.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/gemini.html new file mode 100644 index 0000000..5a8ba0a --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/gemini.html @@ -0,0 +1,56 @@ +{% load static %} + + + + + + + Gemini AI + + + + + + +
+{% include 'nav.html' %} + +

Google Gemini

+ +

+Google Gemini is a recently launched family of large language models (LLMs) created by Google DeepMind. It's considered their most capable AI model yet, designed to compete with OpenAI's GPT-4. +

+ +

+Here's a breakdown of what Gemini offers: +

+ + + +
+ +

+Overall, Google Gemini is a significant development in AI, offering a powerful and versatile suite of tools for developers and users alike. +

+ +{% include 'footer.html' %} +
+ + + + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/index.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/index.html new file mode 100644 index 0000000..a5622ea --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/index.html @@ -0,0 +1,117 @@ +{% load static %} + + + + + + + Gemini AI + + + + + + + + +
+{% include 'nav.html' %} + + +
+
+

What would you like to know?

+

+ This AI model's knowledge cutoff is April 2023. +

+

+ Enter a question for Artificial Intelligence to answer. +

+
+ + +
+
+
+
+ + +
+ + + + +
+
+
+ + +
+ +
+ +
+ +
+
+
+
+ {% include 'footer.html' %} +
+
+ + + + + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/nav.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/nav.html new file mode 100644 index 0000000..3a3545b --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/templates/nav.html @@ -0,0 +1,22 @@ + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/urls.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/urls.py new file mode 100644 index 0000000..01674ee --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/urls.py @@ -0,0 +1,32 @@ +""" +URL configuration for geminiapp project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +from . import views + +urlpatterns = [ +# path('admin/', admin.site.urls), + path('', views.index, name='index'), + path('about', views.about, name='about'), + path('gemini', views.gemini, name='gemini'), + path('ask', views.ask, name='ask'), + path('favicon.ico', views.favicon, name='favicon'), + path('static/favicon.ico', views.favicon, name='favicon'), + path('app.js', views.app_js, name='app_js'), + path('static/app.js', views.app_js, name='app_js'), +] diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/views.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/views.py new file mode 100644 index 0000000..19b7a0b --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/views.py @@ -0,0 +1,121 @@ +''' +This application implements the Google Gemini REST API +''' + +import os +import json + +from django.http import HttpResponse, JsonResponse +from django.views.decorators.csrf import csrf_exempt, csrf_protect, ensure_csrf_cookie +from django.template import loader + +#------------------------------------------------------------------------------- +# This local module fetches the Gemini API Key stored in Google Secrets Manager +#------------------------------------------------------------------------------- + +from .gcp_secrets import init_secrets + +#------------------------------------------------------------------------------- +# This module implements the Google Gemini REST API +#------------------------------------------------------------------------------- + +from .gcp_gemini import ask_gemini + +#------------------------------------------------------------------------------- +# This module implements various Google Cloud Project functions +#------------------------------------------------------------------------------- + +from .gcp_utils import get_project_id # , get_client_ip, get_host + +#------------------------------------------------------------------------------- +# Set the Project ID for Secrets Manager when running locally. +# In Cloud Run the Project ID will be read from the Metadata service +#------------------------------------------------------------------------------- + +gcp_project_id = os.environ.get("GCP_PROJECT_ID", None) + +#------------------------------------------------------------------------------- +# Google Cloud Secret Manager secret name +#------------------------------------------------------------------------------- + +SECRET_NAME = "GEMINI_API_KEY" + +#------------------------------------------------------------------------------- +# The Gemini API Key read from Google Secrets Manager +#------------------------------------------------------------------------------- + +gemini_api_key = None + +#------------------------------------------------------------------------------- +# Utils +#------------------------------------------------------------------------------- + +def create_response(msg): + ''' Accepts a string (msg) and returns JSON that the web browser app.js expects ''' + resp = { "text": msg } + return resp + +@ensure_csrf_cookie +def index(request, *args, **kwargs): + ''' This view serves the home (index) page ''' + ''' TODO: FIX code to handle Client IP in template ''' + template = loader.get_template('index.html') + return HttpResponse(template.render()) + +def app_js(request, *args, **kwargs): + ''' This view serves the app.js file ''' + with open("geminiapp/static/app.js", 'rb') as f: + data = f.read() + + return HttpResponse(data, headers={'Content-Type': 'text/javascript'}) + +def favicon(request, *args, **kwargs): + ''' This view serves the favicon.ico image ''' + with open("geminiapp/static/favicon.ico", 'rb') as f: + data = f.read() + + return HttpResponse(data, headers={'Content-Type': 'image/vnd.microsoft.icon'}) + +def about(request, *args, **kwargs): + ''' This view serves the about page ''' + ''' TODO: FIX code to handle Client IP in template ''' + template = loader.get_template('about.html') + return HttpResponse(template.render()) + +def gemini(request, *args, **kwargs): + ''' This view serves the gemini page ''' + ''' TODO: FIX code to handle Client IP in template ''' + template = loader.get_template('gemini.html') + return HttpResponse(template.render()) + +# @csrf_exempt # use when testing to turn off CSRF +@csrf_protect +def ask(request, *args, **kwargs): + ''' Endpoint that accepts a JSON POST request ''' + data = json.loads(request.body) + + model = data["model"] + question = data["text"] + + answer = ask_gemini(gemini_api_key, model, question) + + return JsonResponse(create_response(answer)) + +#------------------------------------------------------------------------------- +# BEGIN - Initialize app +#------------------------------------------------------------------------------- + +if gcp_project_id is None: + project_id = get_project_id() + if project_id is not None: + gcp_project_id = project_id +if gcp_project_id is None: + print("Error: Cannot set the Project ID") + +if gemini_api_key is None: + gemini_api_key = init_secrets(gcp_project_id, SECRET_NAME) +if gemini_api_key is None: + print("Error: Cannot fetch Gemini API Key") + +# print(f"Project ID: Key {gcp_project_id}") # debug +# print(f"Gemini API Key: {gemini_api_key}") # debug diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/wsgi.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/wsgi.py new file mode 100644 index 0000000..604a918 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/geminiapp/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for geminiapp project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geminiapp.settings') + +application = get_wsgi_application() diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/manage.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/manage.py new file mode 100644 index 0000000..b35fd65 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/manage.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys +from django.core.management.commands.runserver import Command as runserver +import shutil + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'geminiapp.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + +try: + path = os.getcwd() + '/db.sqlite3' + + # print("Copy database") # debug + # print(f"Path: {path}") # debug + + shutil.copyfile(path, '/tmp/db.sqlite3') +except Exception as e: + print(e) # print error message + +if __name__ == '__main__': + port = int(os.environ.get("PORT", 8080)) + runserver.default_addr = "0.0.0.0" + runserver.default_addr_ipv6 = "::" + runserver.default_port = port + main() diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/requirements.txt b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/requirements.txt new file mode 100644 index 0000000..7ddfed7 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/requirements.txt @@ -0,0 +1,4 @@ +Django>=5.0.3 +django-extensions>=3.2.3 +google-cloud-secret-manager>=2.19 +markdown diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_build.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_build.sh new file mode 100644 index 0000000..e06837b --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_build.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +if [[ -z "${GCP_PROJECT_ID}" ]]; +then + echo Please define the environment variable GCP_PROJECT_ID + exit 1 +fi + +# GCP_PROJECT_ID= +REGION=us-central1 +SERVICE_NAME=gemini-python-django-v0 +IMAGE_NAME=gemini-python-django-v0 +LOCATION=us-central1 +REPOSITORY=gemini-project + +echo Building image $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME + +gcloud builds submit \ +--tag $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME +RET=$? + +if [ $RET -eq 0 ]; +then + exit 0 +fi + +echo "***************************************************************" +echo "Build Failed Build Failed Build Failed Build Failed" +echo "***************************************************************" + +exit 0 diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_check_build_upload.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_check_build_upload.sh new file mode 100644 index 0000000..5a209c9 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_check_build_upload.sh @@ -0,0 +1 @@ +gcloud meta list-files-for-upload diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_deploy.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_deploy.sh new file mode 100644 index 0000000..60cd1d6 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/linux/gcp_deploy.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +if [[ -z "${GCP_PROJECT_ID}" ]]; +then + echo Please define the environment variable GCP_PROJECT_ID + exit 1 +fi + +# GCP_PROJECT_ID= +REGION=us-central1 +SERVICE_NAME=gemini-python-django-v0 +IMAGE_NAME=gemini-python-django-v0 +LOCATION=us-central1 +REPOSITORY=gemini-project + +echo Deploying image $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME + +gcloud run deploy $SERVICE_NAME \ +--region $REGION \ +--image $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME \ +--execution-environment=gen1 \ +--memory=256Mi \ +--allow-unauthenticated \ +--platform managed +RET=$? + +if [ $RET -eq 0 ]; +then + exit 0 +fi + +echo "***************************************************************" +echo "Deplopy Failed Deplopy Failed Deplopy Failed Deplopy Failed" +echo "***************************************************************" + +exit 0 diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_build.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_build.bat new file mode 100644 index 0000000..4b2ceea --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_build.bat @@ -0,0 +1,29 @@ +@if not defined GCP_PROJECT_ID ( + @echo Please define the environment variable GCP_PROJECT_ID + Exit /B 1 +) + +:: @set GCP_PROJECT_ID= +@set REGION=us-central1 +@set SERVICE_NAME=gemini-python-django-v0 +@set IMAGE_NAME=gemini-python-django-v0 +@set LOCATION=us-central1 +@set REPOSITORY=gemini-project + +:: Items that must be done before deploying to Google Cloud Run +del \tmp\db.sqlite3 +python manage.py migrate +copy \tmp\db.sqlite3 + +call gcloud builds submit ^ +--tag %LOCATION%-docker.pkg.dev/%GCP_PROJECT_ID%/%REPOSITORY%/%IMAGE_NAME% +@if errorlevel 1 goto err_out + +goto end + +:err_out +@echo *************************************************************** +@echo Build Failed Build Failed Build Failed Build Failed +@echo *************************************************************** + +:end diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_check_build_upload.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_check_build_upload.bat new file mode 100644 index 0000000..5a209c9 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_check_build_upload.bat @@ -0,0 +1 @@ +gcloud meta list-files-for-upload diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_deploy.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_deploy.bat new file mode 100644 index 0000000..3c4238c --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Django/tools/windows/gcp_deploy.bat @@ -0,0 +1,29 @@ +@if not defined GCP_PROJECT_ID ( + @echo Please define the environment variable GCP_PROJECT_ID + Exit /B 1 +) + +:: @set GCP_PROJECT_ID= +@set REGION=us-central1 +@set SERVICE_NAME=gemini-python-django-v0 +@set IMAGE_NAME=gemini-python-django-v0 +@set LOCATION=us-central1 +@set REPOSITORY=gemini-project + +call gcloud run deploy %SERVICE_NAME% ^ +--region %REGION% ^ +--image %LOCATION%-docker.pkg.dev/%GCP_PROJECT_ID%/%REPOSITORY%/%IMAGE_NAME% ^ +--execution-environment=gen1 ^ +--memory=256Mi ^ +--allow-unauthenticated ^ +--platform managed +@if errorlevel 1 goto err_out + +goto end + +:err_out +@echo *************************************************************** +@echo Deplopy Failed Deplopy Failed Deplopy Failed Deplopy Failed +@echo *************************************************************** + +:end diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.dockerignore b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.dockerignore new file mode 100644 index 0000000..a9506e6 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.dockerignore @@ -0,0 +1,15 @@ +.dockerignore +.gitignore +.pylintrc +Dockerfile +LICENSE +*.bat +*.md +*.sh +**.swp +notes.txt +save/ +tools/ +venv/ +__pycache__ +x diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.gcloudignore b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.gcloudignore new file mode 100644 index 0000000..008e213 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.gcloudignore @@ -0,0 +1,14 @@ +__pycache__ +.dockerignore +.pylintrc +.gitignore +.gcloudignore +*.bat +*.md +*.sh +*.swp +LICENSE +notes.txt +save/ +venv/ +tools/ diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.pylintrc b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.pylintrc new file mode 100644 index 0000000..50d9b8c --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/.pylintrc @@ -0,0 +1,5 @@ +[MESSAGES CONTROL] +disable=invalid-name, broad-exception-caught + +[FORMAT] +indent-string="\t" diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_DOCKER.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_DOCKER.md new file mode 100644 index 0000000..fca2040 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_DOCKER.md @@ -0,0 +1,29 @@ +### Build and Run locally with Docker +TODO: Publish my Docker container build and run tools. + +TODO: Publish my Docker tools + +### Example command to build the container: + + - **docker build -t gemini-python-flask .** + +### Example script to run the container (Docker on Windows): + + - Notice some of the options to use a service account inside the container. On my system, I keep secrets, services accounts, etc in a special directory. This command is setup for development and testing. + +``` +@if not defined GCP_PROJECT_ID ( + @echo Please define the environment variable GCP_PROJECT_ID + Exit /B 1 +) + +docker run -it --rm --name gemini-python-flask ^ +-p 8080:8080 ^ +-v %cd%:/work ^ +-v %APPDATA%\gcloud:/root/.config ^ +-v c:/config:/config ^ +-e GOOGLE_APPLICATION_CREDENTIALS=/config/service-account.json ^ +-e GCP_PROJECT_ID=%GCP_PROJECT_ID% ^ +-e FLASK_DEBUG=True ^ +gemini-python-flask +``` diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_LINUX.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_LINUX.md new file mode 100644 index 0000000..2adcf96 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_LINUX.md @@ -0,0 +1,51 @@ +### Testing on Linux + + - This application is tested with Python 3.12. + - From this directory create a Python virtual environment: + + - **python -m venv venv** + - **venv/Scripts/activate.sh** + + - Install dependencies + - **python -m pip install -r requirements.txt** + + - Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project. + - Example: `export GCP_PROJECT_ID=myproject-123456`. + + - Configure Google Cloud Secrets Manager with your Google Gemini API Key. + + - Run the application + - **export FLASK_DEBUG=True** + - **python app.py** + + - Launch a web browser and connect to **http://localhost:8080/** + +### Linux Tools for Google Cloud + +The **tools/linux** directory contains shell scripts to build and deploy to Google Cloud Run: + +- **gcp_build.sh** - Builds the container using Google Cloud Build to Google Artifact Registry. +- **gcp_deploy.sh** - Deploys the container from Google Artifact Registry to Google Cloud Run. +- **gcp_check_build_upload.sh** - Output a list of files that will be upload to Google Cloud Buil. Run this command to make sure only required files are uploaded. Runs the command **gcloud meta list-files-for-upload**. + +Review both files and make any desired changes to the region, location, repository, etc. The changes must match in both files. + + REGION=us-central1 + SERVICE_NAME=gemini-python-flask-v0 + IMAGE_NAME=gemini-python-flask-v0 + LOCATION=us-central1 + REPOSITORY=gemini-project + +### Build and Deploy from Linux or WSL +1. OPTIONAL. From this directory execute `source ./add_tools.sh`. This adds the **tools/linux** directory to the PATH. The alternate is to specify the build tool using the syntax **tools/windows/TOOLNAME**. + + - Example: **./tools/linux/gcp_build.sh** +2. Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project. + + - Example: `export GCP_PROJECT_ID=myproject-123456`. +3. Verify the list of files to be included in the container image: + - gcp_check_build_upload.sh** +4. To build the container execute: + - **gcp_build.sh** +5. To deploy the container to Cloud Run execute: + - **gcp_deploy.sh** diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_WINDOWS.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_WINDOWS.md new file mode 100644 index 0000000..80ab5aa --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/BUILD_WINDOWS.md @@ -0,0 +1,51 @@ +### Testing on Windows + + - This application is tested with Python 3.12. + - From this directory create a Python virtual environment: + + - **python -m venv venv** + - **venv\Scripts\activate.bat** + + - Install dependencies + - **python -m pip install -r requirements.txt** + + - Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project. + - Example: `set GCP_PROJECT_ID=myproject-123456`. + + - Configure Google Cloud Secrets Manager with your Google Gemini API Key. + + - Run the application + - **set FLASK_DEBUG=True** + - **python app.py** + + - Launch a web browser and connect to **http://localhost:8080/** + +### Windows Tools for Google Cloud + +The **tools\windows** directory contains batch files to build and deploy to Google Cloud Run: + +- **gcp_build.bat** - Builds the container using Google Cloud Build to Google Artifact Registry. +- **gcp_deploy.bat** - Deploys the container from Google Artifact Registry to Google Cloud Run. +- **gcp_check_build_upload.bat** - Output a list of files that will be upload to Google Cloud Buil. Run this command to make sure only required files are uploaded. Runs the command **gcloud meta list-files-for-upload**. + +Review both files and make any desired changes to the region, location, repository, etc. The changes must match in both files. + + @set REGION=us-central1 + @set SERVICE_NAME=gemini-python-flask-v0 + @set IMAGE_NAME=gemini-python-flask-v0 + @set LOCATION=us-central1 + @set REPOSITORY=gemini-project + +### Build and Deploy from Windows +1. OPTIONAL. From this directory execute `add_tools.bat`. This adds the **tools\windows** directory to the PATH. The alternate is to specify the build tool using the syntax **tools\windows\TOOLNAME**. + + - Example: **.\tools\windows\gcp_build.bat** +2. Set the environment variable **GCP_PROJECT_ID** to the Google Cloud Project. + + - Example: `set GCP_PROJECT_ID=myproject-123456`. +3. Verify the list of files to be included in the container image: + - gcp_check_build_upload.bat** +4. To build the container execute: + - **gcp_build.bat** +5. To deploy the container to Cloud Run execute: + - **gcp_deploy.bat** diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/Dockerfile b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/Dockerfile new file mode 100644 index 0000000..5968b2f --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/Dockerfile @@ -0,0 +1,41 @@ +# Use the official Python 3 image. +# https://hub.docker.com/_/python +# +# python:3 builds a 1060 MB image - 342 MB in Google Container Registry +# FROM python:3 +# +# python:3-slim builds a 172 MB image - 60 MB in Google Container Registry +# FROM python:3-slim +# +# python:3-alpine builds a 97 MB image - 32 MB in Google Container Registry +FROM python:3-alpine + +# RUN apt-get update -y +# RUN apt-get install -y python-pip + +# Create and change to the app directory. +WORKDIR /app + +COPY . . + +RUN chmod 444 app.py requirements.txt + +RUN adduser app -D app + +# Run the application as a non-root user. +USER app + +# Fix warning message: WARNING: The script is installed in '/home/app/.local/bin' which is not on PATH. +ENV PATH=${PATH}:/home/app/.local/bin + +RUN python -m pip install --no-cache-dir -r requirements.txt + +# Service must listen to $PORT environment variable. +# This default value facilitates local development. +ENV PORT 8080 + +ENV PYTHONUNBUFFERED True + +# Run the web service on container startup. +# CMD [ "python", "app.py" ] +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/LICENSE b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/LICENSE new file mode 100644 index 0000000..f1c7291 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 John J. Hanley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/README.md b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/README.md new file mode 100644 index 0000000..9db947a --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/README.md @@ -0,0 +1,31 @@ +# Gemini Python Flask Application + +This section contains source code for the Python Flask application. + + - Python 3.12 + Flask + +## Build and Deploy to Google Cloud Run + +### Requirements + +#### Google Cloud CLI + + - [Google Cloud CLI](https://cloud.google.com/cli). Tested with version 470 (2024-04-26). + +#### Google Gemini API Key + + - [Google Gemini API Key](https://aistudio.google.com/app/prompts/new_chat/). + +#### Google Cloud Secrets Manager + + - The application reads the Google Gemini API Key in Google Secrets Manager. The secret name is **GEMINI_API_KEY**. + - TODO: Publish my tool to create and rotate the secret. + +#### Google Cloud Run Permissions + - The service account attached to Google Cloud reads the secret from Google Secrets Manager. Add the role **Secret Manager Secret Accessor** to the project's IAM for the service account. + - TODO: Pubish my tools to modify IAM permissions + +Operating System Specific Instructions: + - [Docker](BUILD_DOCKER.md) + - [Linux](BUILD_LINUX.md) + - [Windows](BUILD_WINDOWS.md) diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.bat new file mode 100644 index 0000000..8cfd8f3 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.bat @@ -0,0 +1,5 @@ +set TOOLS=%cd%\tools\windows + +@set PATH=%TOOLS%;%PATH% + +@echo Added tools to the path diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.sh new file mode 100755 index 0000000..9f5b9fa --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/add_tools.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +TOOLS=`pwd`/tools/linux + +export PATH=$PATH:$TOOLS + +echo Added tools to the path diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/app.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/app.py new file mode 100644 index 0000000..7c22fbf --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/app.py @@ -0,0 +1,162 @@ +''' +This application implements the Google Gemini REST API +''' + +import json +import os + +# Flask imports +from flask import Flask, request, Response, send_from_directory, render_template +from flask_wtf.csrf import CSRFProtect + +#------------------------------------------------------------------------------- +# This local module fetches the Gemini API Key stored in Google Secrets Manager +#------------------------------------------------------------------------------- + +from gcp_secrets import init_secrets + +#------------------------------------------------------------------------------- +# This module implements the Google Gemini REST API +#------------------------------------------------------------------------------- + +from gcp_gemini import ask_gemini + +#------------------------------------------------------------------------------- +# This module implements various Google Cloud Project functions +#------------------------------------------------------------------------------- + +from gcp_utils import get_project_id, get_client_ip + +#------------------------------------------------------------------------------- +# Set the Project ID for Secrets Manager when running locally. +# In Cloud Run the Project ID will be read from the Metadata service +#------------------------------------------------------------------------------- + +gcp_project_id = os.environ.get("GCP_PROJECT_ID", None) + +#------------------------------------------------------------------------------- +# Google Cloud Secret Manager secret name +#------------------------------------------------------------------------------- + +SECRET_NAME = "GEMINI_API_KEY" + +#------------------------------------------------------------------------------- +# The Gemini API Key read from Google Secrets Manager +#------------------------------------------------------------------------------- + +gemini_api_key = 'AIzaSyBmdRFzwspedvRUsH34tTjZbGcNpfLVkko' + +#------------------------------------------------------------------------------- +# Create the Flask application +#------------------------------------------------------------------------------- + +app = Flask(__name__) + +#------------------------------------------------------------------------------- +# TODO: Update with a different value +#------------------------------------------------------------------------------- + +app.config["SECRET_KEY"] = "9b37d4693c274cc553524218819305e3fa739a12ed84dc7b43d882d6c3320004" + +#------------------------------------------------------------------------------- +# CSRF - Cross Site Request Forgery +#------------------------------------------------------------------------------- + +# app.config['WTF_CSRF_ENABLED'] = False +csrf = CSRFProtect(app) + +#------------------------------------------------------------------------------- +# Utils +#------------------------------------------------------------------------------- + +def create_response(msg): + ''' Accepts a string (msg) and returns JSON that the web browser app.js expects ''' + resp = { "text": msg } + return json.dumps(resp) + +#------------------------------------------------------------------------------- +# Routes +#------------------------------------------------------------------------------- + +@app.after_request +def after_request(response): + ''' + Flask does not log the correct client IP address on Cloud Run. + Flask logs the proxy (GFE) address when running on Cloud Run. + This function parses the HTTP request headers to determine the correct address. + ''' + + # print(f"Host: {get_host()}") + print(f"Client IP: {get_client_ip()}") + return response + +@app.route("/", methods = ["GET"]) +def home(): + ''' Return the website home page ''' + return render_template("index.html", client_ip=get_client_ip()) + +@app.route("/about", methods = ["GET"]) +def about(): + ''' Return the website about page ''' + return render_template("about.html", client_ip=get_client_ip()) + +@app.route("/gemini", methods = ["GET"]) +def gemini(): + ''' Return the website gemini page ''' + return render_template("gemini.html", client_ip=get_client_ip()) + +@app.route("/app.js", methods = ["GET"]) +def app_js(): + ''' Return the file app.js ''' + return send_from_directory("static", "app.js") + +@app.route("/favicon.ico", methods = ["GET"]) +def fav_ico(): + ''' Return the file favicon.ico ''' + return send_from_directory("static", "favicon.ico", mimetype='image/vnd.microsoft.icon') + +# @csrf.exempt +@app.route("/ask", methods = ["POST"]) +def ask(): + ''' Endpoint that accepts a JSON POST request ''' + data = request.get_json() + + model = data.get("model") + question = data.get("text") + + answer = ask_gemini(gemini_api_key, model, question) + + return Response(create_response(answer), mimetype='application/json') + +#------------------------------------------------------------------------------- +# BEGIN - Initialize app +#------------------------------------------------------------------------------- + +if gcp_project_id is None: + project_id = get_project_id() + if project_id is not None: + gcp_project_id = project_id +if gcp_project_id is None: + print("Error: Cannot set the Project ID") + +if gemini_api_key is None: + gemini_api_key = init_secrets(gcp_project_id, SECRET_NAME) +if gemini_api_key is None: + print("Error: Cannot fetch Gemini API Key") + +#------------------------------------------------------------------------------- +# This section only runs if started by Python +# Does not run under gunicorn +#------------------------------------------------------------------------------- + +if __name__ == "__main__": + debugFlag = os.environ.get("FLASK_DEBUG", False) + + if debugFlag == "False": + debugFlag = False + print('Debug disable') + elif debugFlag == "True": + debugFlag = True + print('Debug enabled') + + app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini.py new file mode 100644 index 0000000..6aae4ca --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini.py @@ -0,0 +1,144 @@ +''' +This module implements the Google Gemini REST API + https://ai.google.dev/tutorials/rest_quickstart +''' + +import json +import requests + +#------------------------------------------------------------------------------- +# Markdown to HTML converter +#------------------------------------------------------------------------------- + +import markdown + +#------------------------------------------------------------------------------- +# This module implements utilities for the Google Gemini REST API +#------------------------------------------------------------------------------- + +from gcp_gemini_utils import get_gemini_model_endpoint, get_gemini_response_text + +def ask_gemini(api_key, model, question): + ''' Interact with Google Gemini ''' + + #--------------------------------------------------------------------- + # Google Gemini REST API Documentation + # https://ai.google.dev/api/rest + #--------------------------------------------------------------------- + + #--------------------------------------------------------------------- + # Validate api_key + #--------------------------------------------------------------------- + + if api_key is None or len(api_key) == 0: + return "Internal Error: Missing API Key" + + #--------------------------------------------------------------------- + # Verify question parameter + # + # TODO: Validate question + # Only the string length for zero is checked at this time. + # What is the maximum length supported. Gemini specifies token + # which is about 4 characters. + # https://ai.google.dev/models/gemini + # Gemini 1.0 Pro input token limit is 30,720. + # Gemini 1.5 Pro input token limit is 1,048,576. + #--------------------------------------------------------------------- + + if question is None or len(question) == 0: + return "Please enter a question" + + #--------------------------------------------------------------------- + # Get the Gemini Model to use + #--------------------------------------------------------------------- + + url = get_gemini_model_endpoint(model) + + headers = { + "x-goog-api-key": api_key, + "Content-type": "application/json" + } + + #--------------------------------------------------------------------- + # Format the JSON request + #--------------------------------------------------------------------- + + data = { + "contents": [ + { + "parts":[ + { + "text": question + } + ] + } + ] + } + + #--------------------------------------------------------------------- + # Issue the HTTP POST request + #--------------------------------------------------------------------- + + try: + # print(json.dumps(data, indent=4)) # debug + + response = requests.post(url, headers=headers, json=data, timeout=120) + + # print(response.status_code) # debug + # print(response.content) # debug + + if response.status_code == 404: + print(response.content) # print error message + msg = "**Error: The Gemini model is not available for your project or does not exist**" + return markdown.markdown(msg) + + if response.status_code >= 400: + print(response.content) # print error message + msg = "**Error: Request to Gemini failed**" + return markdown.markdown(msg) + + #--------------------------------------------------------------------- + # Process the output + # TODO: This needs better handling. + # Gemini returns various formats that are not yet documented. + # An important item is to process the key "finishReason". + # Normal requests return "STOP", but I have seen "SAFETY" + # which means the request was rejected. See the link: + # https://ai.google.dev/api/rest/v1/GenerateContentResponse#FinishReason + # + # https://ai.google.dev/api/rest/v1/Content + #--------------------------------------------------------------------- + + resp = response.json() + + # print(json.dumps(resp, indent=4)) # debug + + reason = resp["candidates"][0]["finishReason"] + + # print("Reason:", reason) # debug + + if reason == "STOP": + # Good status + text = get_gemini_response_text(resp) + elif reason == "MAX_TOKENS": + # Error + text = "**Gemini Error: The maximum number of tokens as specified in the request was reached.**" + elif reason == "RECITATION": + # Error + text = "**Gemini Error: The candidate content was flagged for recitation reasons.**" + elif reason == "SAFETY": + # Error + text = "**Gemini Error: The candidate content was flagged for safety reasons.**" + else: + # Error + text = f"**Gemini Error: Gemini refused the question for {reason}**" + + #--------------------------------------------------------------------- + # Gemini returns Markdown, convert to HTML + #--------------------------------------------------------------------- + + html = markdown.markdown(text, extensions=['tables']) + return html.replace('
', '
') + except Exception as e: + print(e) # print error message + return None diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini_utils.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini_utils.py new file mode 100644 index 0000000..640a9fc --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_gemini_utils.py @@ -0,0 +1,48 @@ +''' +This module implements utilities for the Google Gemini REST API + https://ai.google.dev/tutorials/rest_quickstart +''' + +def get_gemini_model_endpoint(model): + ''' Determine which Gemini model to use and return the REST URL ''' + + host = "https://generativelanguage.googleapis.com" + + #--------------------------------------------------------------------- + # Validate model + #--------------------------------------------------------------------- + + if model is None or len(model) == 0: + # Default to model (Gemini 1.0 Pro) + model = "gemini_1_0_pro_latest" + + #--------------------------------------------------------------------- + # Select the Gemini LLM model to use + #--------------------------------------------------------------------- + + if model == "gemini_1_0_pro_latest": + path = "/v1beta/models/gemini-1.0-pro-latest:generateContent" + elif model == "gemini_1_0_ultra_latest": + path = "/v1beta/models/gemini-1.0-ultra-latest:generateContent" + elif model == "gemini_1_5_pro_latest": + path = "/v1beta/models/gemini-1.5-pro-latest:generateContent" + else: + # Default model (Gemini 1.0 Pro) + path = "/v1beta/models/gemini-pro:generateContent" + + return host + path + +def get_gemini_response_text(resp): + ''' Process the Gemini response and return the content text ''' + + text = '' + + candidates = resp["candidates"] + + for candidate in candidates: + parts = candidate["content"]["parts"] + + for part in parts: + text += part.get("text", "") + + return text diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_secrets.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_secrets.py new file mode 100644 index 0000000..8fa7872 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_secrets.py @@ -0,0 +1,55 @@ +''' +This module fetches the Gemini API Key stored in Google Secrets Manager + https://cloud.google.com/python/docs/reference/secretmanager/latest +''' + +# Google Secrets Manager imports +from google.cloud import secretmanager_v1 + +def init_secrets(project_id, secret_name): + ''' Fetch the GEMINI_API_KEY stored in Google Secrets Manager ''' + + try: + #--------------------------------------------------------------------- + # Secrets Manager reports an error if byte strings are used + #--------------------------------------------------------------------- + + if isinstance(project_id, bytes): + project_id = project_id.decode('utf-8') + + if isinstance(secret_name, bytes): + secret_name = secret_name.decode('utf-8') + + #--------------------------------------------------------------------- + # Initialize the Secrets Manager Client + #--------------------------------------------------------------------- + + client = secretmanager_v1.SecretManagerServiceClient() + + #--------------------------------------------------------------------- + # Format the secret name + # In app.py, the secret name is set by SECRET_NAME + #--------------------------------------------------------------------- + + name = f"projects/{project_id}/secrets/{secret_name}/versions/latest" + + #--------------------------------------------------------------------- + # Build the client request + #--------------------------------------------------------------------- + + req = secretmanager_v1.AccessSecretVersionRequest( + name=name + ) + + #--------------------------------------------------------------------- + # Fetch the secret + #--------------------------------------------------------------------- + + response = client.access_secret_version(request=req) + + api_key = response.payload.data.decode('utf-8') + + return api_key + except Exception as e: + print(e) # print error message + return None diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_utils.py b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_utils.py new file mode 100644 index 0000000..d552036 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/gcp_utils.py @@ -0,0 +1,56 @@ +''' +This module implements various Google Cloud Project functions +''' +# Flask imports +from flask import request + +import requests + +def get_project_id(): + ''' + This function reads the Google Cloud Project ID from the Metadata service + ''' + + try: + url = "http://metadata.google.internal/computeMetadata/v1/project/project-id" + # url = "http://metadata.goog/v1/project/project-id" + + headers = { + "Metadata-Flavor": "Google" + } + + response = requests.get(url, headers=headers, timeout=0.5) + + if response.status_code >= 400: + print(response.content) # print error message + return None + + return response.content.decode("utf-8") + except Exception as e: + print(e) # print error message + return None + +def get_client_ip(): + ''' + Returns the client IP address. + That IP might be IPv4 or IPv6 depending on how the client connected. + ''' + + if "x-forwarded-for" in request.headers: + return request.headers.getlist("x-forwarded-for")[0].rpartition(" ")[-1] + + return request.remote_addr + +def get_host(): + ''' + Returns the host header + + On Google Cloud Run the HTTP Host header cannot be forged. + 1) The host header is used by the GFE to know which Cloud Run instance to forware to. + 2) The GFE only forwards HTTPS requets. That means the host header must match + one of the managed certificates. + ''' + if "host" in request.headers: + return request.headers.get("host", "localhost") + + return "localhost" diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/requirements.txt b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/requirements.txt new file mode 100644 index 0000000..766dfc4 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/requirements.txt @@ -0,0 +1,5 @@ +Flask>=3.0 +Flask-WTF>=1.2 +google-cloud-secret-manager>=2.19 +markdown>=3.6 +gunicorn>=21.2 diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/app.js b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/app.js new file mode 100644 index 0000000..18fe53c --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/app.js @@ -0,0 +1,80 @@ +// Toggle the Submit button to a Loading... button, or vice versa +function toggleSubmitButton() { + const submitButton = document.querySelector('#input-form button[type=submit]'); + + // Flip the value true->false or false->true + submitButton.disabled = !submitButton.disabled; + + // Flip the button's text back to "Waiting..."" or "Submit" + const submitButtonText = submitButton.querySelector('.submit-button-text'); + if(submitButtonText.innerHTML === 'Waiting...') { + submitButtonText.innerHTML = 'Submit'; + } else { + submitButtonText.innerHTML = 'Waiting...'; + } + + // Show or Hide the loading spinner + const submitButtonSpinner = submitButton.querySelector('.submit-button-spinner') + submitButtonSpinner.hidden = !submitButtonSpinner.hidden; +} + +// Process the user's form input +function processFormInput(form) { + // Get values from the form + const token = form.csrf_token.value.trim(); + const topic = form.topic.value.trim(); + const model = form.model.value.trim(); + + // Update the Submit button to indicate we're done loading + toggleSubmitButton(); + + // Clear the output of any existing content + document.querySelector('#output').innerHTML = ''; + + // Send the question + send(token, topic, model); +} + +function send(token, topic, model) { + fetch("ask", { + method: "POST", + headers: { + 'X-CSRFToken': token, + 'Content-type': "application/json" + }, + body: JSON.stringify({ token: token, text: topic, model: model }) + }) + .then(response => { + return response.json(); + }) + .then(data => { + document.querySelector('#output').innerHTML = data["text"]; + toggleSubmitButton(); + }) + .catch(error => { + console.log(error) + toggleSubmitButton(); + }) +} + +function main() { + // Wait for the user to submit the form + document.querySelector('#input-form').onsubmit = function(e) { + // Stop the form from submitting, we'll handle it in the browser with JS + e.preventDefault(); + + // Process the data in the form, passing the form to the function + processFormInput(e.target) + }; + + // Update the character count when the user enters any text in the topic textarea + document.querySelector('#topic').oninput = function(e) { + // Get the current length + const length = e.target.value.length; + // Update the badge text + document.querySelector('#topic-badge').innerText = `${length} characters`; + } +} + +// Wait for the DOM to be ready before we start +addEventListener('DOMContentLoaded', main); diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/favicon.ico b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/favicon.ico new file mode 100644 index 0000000..98f74ce Binary files /dev/null and b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/static/favicon.ico differ diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/about.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/about.html new file mode 100644 index 0000000..27cc093 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/about.html @@ -0,0 +1,41 @@ + + + + + + Gemini AI + + + + +
+{% include 'nav.html' %} + +

Gemini Application

+ +

+This application implements the Gemini REST API using ASP.NET Core 8.0 and is deployed on Google Cloud Run. +

+ +

+This application supports sending questions to Google Gemini and displays the response. The purpose is to demonstrate how to interface with Google Gemini. +

+ +

+Three Google Gemini models are supported: +

+

+ +{% include 'footer.html' %} +
+ + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/footer.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/footer.html new file mode 100644 index 0000000..0572e5e --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/footer.html @@ -0,0 +1,12 @@ +
+
+
+
+
+

+ Client IP: {{client_ip}} +
+ Version 0.3.1 - April 3, 2024 - Python 3.12 Flask +
+ © 2024 John Hanley +

diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/gemini.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/gemini.html new file mode 100644 index 0000000..fdff232 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/gemini.html @@ -0,0 +1,52 @@ + + + + + + Gemini AI + + + + +
+{% include 'nav.html' %} + +

Google Gemini

+ +

+Google Gemini is a recently launched family of large language models (LLMs) created by Google DeepMind. It's considered their most capable AI model yet, designed to compete with OpenAI's GPT-4. +

+ +

+Here's a breakdown of what Gemini offers: +

+ + + +
+ +

+Overall, Google Gemini is a significant development in AI, offering a powerful and versatile suite of tools for developers and users alike. +

+ +{% include 'footer.html' %} +
+ + + + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/index.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/index.html new file mode 100644 index 0000000..ad3682f --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/index.html @@ -0,0 +1,114 @@ + + + + + + Gemini AI + + + + + + +
+{% include 'nav.html' %} + + +
+
+

What would you like to know?

+

+ This AI model's knowledge cutoff is April 2023. +

+

+ Enter a question for Artificial Intelligence to answer. +

+
+ + +
+
+
+
+ + +
+ + + + +
+
+
+ + +
+ + +
+ +
+ +
+
+
+
+ {% include 'footer.html' %} +
+
+ + + + + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/nav.html b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/nav.html new file mode 100644 index 0000000..3a3545b --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/templates/nav.html @@ -0,0 +1,22 @@ + + diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/linux/gcp_build.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/linux/gcp_build.sh new file mode 100755 index 0000000..083a1bc --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/linux/gcp_build.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +if [[ -z "${GCP_PROJECT_ID}" ]]; +then + echo Please define the environment variable GCP_PROJECT_ID + exit 1 +fi + +# GCP_PROJECT_ID= +REGION=us-central1 +SERVICE_NAME=gemini-python-flask-v0 +IMAGE_NAME=gemini-python-flask-v0 +LOCATION=us-central1 +REPOSITORY=gemini-project + +echo Building image $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME + +gcloud builds submit \ +--tag $LOCATION-docker.pkg.dev/$GCP_PROJECT_ID/$REPOSITORY/$IMAGE_NAME +RET=$? + +if [ $RET -eq 0 ]; +then + exit 0 +fi + +echo "***************************************************************" +echo "Build Failed Build Failed Build Failed Build Failed" +echo "***************************************************************" + +exit 0 diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/linux/gcp_check_build_upload.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/linux/gcp_check_build_upload.sh new file mode 100755 index 0000000..5a209c9 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/linux/gcp_check_build_upload.sh @@ -0,0 +1 @@ +gcloud meta list-files-for-upload diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/linux/gcp_deploy.sh b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/linux/gcp_deploy.sh new file mode 100755 index 0000000..5b56cda --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/linux/gcp_deploy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +GCP_PROJECT_ID=jobs-long-running +REGION=us-central1 +SERVICE_NAME=gemini-python-flask-v0 +LOCATION=us-central1 + +gcloud run deploy $SERVICE_NAME \ + --source . \ + --region $REGION \ + --allow-unauthenticated \ + --platform managed diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/windows/gcp_build.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/windows/gcp_build.bat new file mode 100644 index 0000000..752018b --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/windows/gcp_build.bat @@ -0,0 +1,24 @@ +@if not defined GCP_PROJECT_ID ( + @echo Please define the environment variable GCP_PROJECT_ID + Exit /B 1 +) + +:: @set GCP_PROJECT_ID= +@set REGION=us-central1 +@set SERVICE_NAME=gemini-python-flask-v0 +@set IMAGE_NAME=gemini-python-flask-v0 +@set LOCATION=us-central1 +@set REPOSITORY=gemini-project + +call gcloud builds submit ^ +--tag %LOCATION%-docker.pkg.dev/%GCP_PROJECT_ID%/%REPOSITORY%/%IMAGE_NAME% +@if errorlevel 1 goto err_out + +goto end + +:err_out +@echo *************************************************************** +@echo Build Failed Build Failed Build Failed Build Failed +@echo *************************************************************** + +:end diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/windows/gcp_check_build_upload.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/windows/gcp_check_build_upload.bat new file mode 100644 index 0000000..5a209c9 --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/windows/gcp_check_build_upload.bat @@ -0,0 +1 @@ +gcloud meta list-files-for-upload diff --git a/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/windows/gcp_deploy.bat b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/windows/gcp_deploy.bat new file mode 100644 index 0000000..5e8391b --- /dev/null +++ b/cloud-run-gemini-chat/Application/CloudRun/Python-Flask/tools/windows/gcp_deploy.bat @@ -0,0 +1,29 @@ +@if not defined GCP_PROJECT_ID ( + @echo Please define the environment variable GCP_PROJECT_ID + Exit /B 1 +) + +:: @set GCP_PROJECT_ID= +@set REGION=us-central1 +@set SERVICE_NAME=gemini-python-flask-v0 +@set IMAGE_NAME=gemini-python-flask-v0 +@set LOCATION=us-central1 +@set REPOSITORY=gemini-project + +call gcloud run deploy %SERVICE_NAME% ^ +--region %REGION% ^ +--image %LOCATION%-docker.pkg.dev/%GCP_PROJECT_ID%/%REPOSITORY%/%IMAGE_NAME% ^ +--execution-environment=gen1 ^ +--memory=256Mi ^ +--allow-unauthenticated ^ +--platform managed +@if errorlevel 1 goto err_out + +goto end + +:err_out +@echo *************************************************************** +@echo Deplopy Failed Deplopy Failed Deplopy Failed Deplopy Failed +@echo *************************************************************** + +:end diff --git a/cloud-run-gemini-chat/Gemini-Errors/example-1.json b/cloud-run-gemini-chat/Gemini-Errors/example-1.json new file mode 100644 index 0000000..befc1c1 --- /dev/null +++ b/cloud-run-gemini-chat/Gemini-Errors/example-1.json @@ -0,0 +1,46 @@ +{ + "candidates": [ + { + "finishReason": "SAFETY", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "LOW" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "MEDIUM" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/cloud-run-gemini-chat/Gemini-Errors/example-2.json b/cloud-run-gemini-chat/Gemini-Errors/example-2.json new file mode 100644 index 0000000..e042b62 --- /dev/null +++ b/cloud-run-gemini-chat/Gemini-Errors/example-2.json @@ -0,0 +1,54 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "13" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/cloud-run-gemini-chat/LICENSE b/cloud-run-gemini-chat/LICENSE new file mode 100644 index 0000000..f1c7291 --- /dev/null +++ b/cloud-run-gemini-chat/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 John J. Hanley + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cloud-run-gemini-chat/README.md b/cloud-run-gemini-chat/README.md new file mode 100644 index 0000000..5b568e1 --- /dev/null +++ b/cloud-run-gemini-chat/README.md @@ -0,0 +1,25 @@ +# Gemini Application + +This repository contains source code for several website applications implemented in different languages and frameworks. + + - ASP.NET Core 8.0 Blazor + - Node.js 20.12 + Express.js + - PHP 8.3 + Apache + - Python 3.12 + Django + - Python 3.12 + Flask + +Each application implements the [Gemini REST API](https://ai.google.dev/tutorials/rest_quickstart) and is deployed on Google Cloud Run. + +Each application supports sending questions to Google Gemini and displaying the response. The purpose is to demonstrate how to interface with Google Gemini. + +Three Google Gemini models are supported: + + - Google Gemini 1.0 Pro + - Google Gemini 1.0 Ultra + - Google Gemini 1.5 Pro + +## Example Application Screenshot + +ASP.NET Blazor deployed on Google Cloud Run. + +![alt text](resources/screenshot-browser.png "Title") diff --git a/cloud-run-gemini-chat/resources/screenshot-browser.png b/cloud-run-gemini-chat/resources/screenshot-browser.png new file mode 100644 index 0000000..963d24a Binary files /dev/null and b/cloud-run-gemini-chat/resources/screenshot-browser.png differ