Last active
February 9, 2025 16:44
-
-
Save brainysmurf/1e086df246ad65d36e0b7c0fef11fc5f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(global,name,Package,helpers){var ref=function wrapper(args){var wrapped=function(){return Package.apply(global.Import&&Import.module?Import._import(name):global[name],[global].concat(Array.prototype.slice.call(arguments)))};for(i in args){wrapped[i]=args[i]}return wrapped}(helpers);if(global.Import&&Import.module){Import.register(name,ref)}else{Object.defineProperty(global,name,{value:ref});global.Import=global.Import||function(lib){return global[lib]};Import.module=false}})(this, | |
"Requests", | |
function RequestsPackage_ (global, config) { | |
var self = this, discovery, discoverUrl; | |
discovery = function (name, version) { | |
return self().get('https://www.googleapis.com/discovery/v1/apis/' + name + '/' + version + '/rest'); | |
}; | |
discoverUrl = function (name, version, category, method) { | |
var data; | |
data = discovery(name, version).json(); | |
return data.baseUrl + data.resources[category].methods[method].path; | |
}; | |
config = config || {}; | |
config.baseUrl = config.baseUrl || ""; | |
config.body = config.body || {}; | |
config.headers = config.headers || null; | |
config.query = config.query || {}; | |
config.oauth = config.oauth || false; | |
config.basicAuth = config.basicAuth || false; | |
config.discovery = config.discovery || null; | |
if (config.discovery && config.discovery.name && config.discovery.version && config.discovery.category && config.discovery.method) { | |
config.baseUrl = discoverUrl(config.discovery.name, config.discovery.version, config.discovery.category, config.discovery.method); | |
} | |
if (config.oauth) { | |
config.headers = config.headers || {}; | |
config.headers['Authorization'] = "Bearer " + (config.oauth === 'me' ? global['Script' + 'App'].getOAuthToken() : config.oauth); | |
} | |
if (config.basicAuth) { | |
config.headers = config.headers || {}; | |
config.headers['Authorization'] = "Basic " + config.basicAuth; | |
} | |
var Response = function (_resp) { | |
return { | |
json: function () { | |
try { | |
return JSON.parse(this.text()); | |
} catch (err) { | |
Logger.log(this.text()); | |
throw Error("Response did not return a parsable json object"); | |
} | |
}, | |
text: function () { | |
return _resp.getContentText(); | |
}, | |
statusCode: function () { | |
return _resp.getResponseCode(); | |
}, | |
getAllHeaders: function () { | |
return _resp.getAllHeaders(); | |
}, | |
/* | |
Return true if encounted rate limit | |
*/ | |
hitRateLimit: function () { | |
if (this.statusCode() === 429) { | |
var headers = this.getAllHeaders(); | |
var header_reset_at = headers['x-ratelimit-reset']; | |
header_reset_at = header_reset_at.replace(" UTC", "+0000").replace(" ", "T"); | |
var reset_at = new Date(header_reset_at).getTime(); | |
var utf_now = new Date().getTime(); | |
var milliseconds = reset_at - utf_now + 10; | |
if (milliseconds > 0) { | |
console.log("Sleeping for " + (milliseconds/1000).toString() + " seconds."); | |
Utilities.sleep(milliseconds); | |
} | |
return true; | |
} | |
return false; | |
}, | |
/* | |
*/ | |
paged: function (rootKey) { | |
if (typeof rootKey === 'undefined') throw Error('Specify path'); | |
var json; | |
json = this.json(); | |
var page, batch, req, rawRequest; | |
batch = new BatchRequests(); | |
page = 2; | |
while (json.meta.total_pages >= page) { | |
req = this.request; | |
req.setQuery('page', page); | |
rawRequest = req.toRequestObject(); | |
batch.add( rawRequest ); | |
page++; | |
} | |
var fetchResult = batch.fetchAll(); | |
var zipResult = fetchResult.zip(rootKey, json); | |
return zipResult; | |
}, | |
iterPaging: function (rootKey, next, query) { | |
next = next || 'nextPageToken'; | |
query = query || 'pageToken'; | |
if (typeof rootKey === 'undefined') throw Error('Specify root key'); | |
var json; | |
json = this.json(); | |
var token, collection = [], req, result, j; | |
Array.prototype.push.apply(collection, json[rootKey]); | |
token = json[next]; | |
while (token) { | |
req = this.request; | |
req.setQuery(query, token); | |
result = req.fetch(); | |
if (!result.ok) { | |
// "If the token is rejected for any reason, it should be discarded, and pagination should be restarted from the first page of results." | |
req.clearQuery(); | |
result = req.fetch(); | |
if (!result.ok) { | |
throw Error("Unable to reach " + req.getUrl() + " status code: " + req.statusCode()); | |
} | |
} | |
j = result.json(); | |
Array.prototype.push.apply(collection, j[rootKey]); | |
token = j[next]; | |
} | |
return collection; | |
}, | |
ok: _resp.getResponseCode() == 200 | |
} | |
}; | |
var BatchResponses = function (_responses) { | |
_responses = _responses || []; | |
return { | |
/* | |
Used to process when a single object is returned and need it to be a list | |
*/ | |
objects: function (rootKey, options) { | |
var objectList; | |
options = options || {}; | |
objectList = options.initialValue || []; | |
_responses.forEach(function (resp) { | |
var j; | |
j = resp.json(); | |
objectList.push(j[rootKey]); | |
}); | |
return objectList; | |
}, | |
/* | |
Used to process batch items that return a list at a particul rootKey | |
*/ | |
zip: function (rootKey, options) { | |
var json; | |
options = options || {}; | |
json = options.initialValue || []; | |
options.everyObj = options.everyObj || {}; | |
_responses.forEach(function (resp) { | |
var j, req, match, key; | |
j = resp.json(); | |
req = resp.request; | |
if ((j[rootKey] || {length: 0}).length > 0) { | |
j[rootKey].forEach(function (o) { | |
for (key in options.everyObj) { | |
if (options.everyObj[key] instanceof RegExp) { | |
match = req.getUrl().match(options.everyObj[key]); | |
o[key] = parseInt(match[1]); // FIXME: Need to be able to define a callback function to process | |
} else if (options.everyObj[key] instanceof Object && options.everyObj[key].query) { | |
o[key] = req.getQuery()[options.everyObj[key].query]; | |
} else { | |
o[key] = options.everyObj[key]; | |
} | |
} | |
}); | |
Array.prototype.push.apply(json, j[rootKey]); | |
} | |
}); | |
return json; | |
}, | |
getResponses: function () { | |
return _responses; | |
} | |
}; | |
}; | |
BatchResponses.empty = function () { | |
return new this([]); | |
}; | |
var BatchRequests = function(_list) { | |
_list = _list || []; | |
return { | |
fetchAll: function (expandForPages) { | |
expandForPages = expandForPages || false; | |
var responses, expandedRequests; | |
if (_list.length === 0) return BatchResponses.empty(); | |
expandedRequests = new BatchRequests(); | |
responses = UrlFetchApp.fetchAll(_list).reduce(function (acc, response, index) { | |
var resp, origRequest ,url; | |
origRequest = _list[index]; | |
resp = new Response(response); | |
resp.request = new Request(origRequest); | |
if (resp.hitRateLimit()) { | |
var request, r; | |
r = resp.request; | |
resp = resp.request.fetch(); | |
resp.request = r; | |
} | |
acc.push(resp); | |
if (expandForPages && (function (r, er) { | |
var json, page, req, rawRequest; | |
json = r.json(); | |
page = 2; | |
while ((json.meta || {total_page: -1}).total_pages >= page) { | |
req = r.request; | |
req.setQuery('page', page); | |
rawRequest = req.toRequestObject(); | |
er.add( rawRequest ); | |
page++; | |
} | |
})(resp, expandedRequests)); | |
return acc; | |
}, []); | |
if (expandForPages && (function (resps) { | |
var newResponses; | |
if (expandedRequests.length === 0) return; // no need to continue | |
newResponses = expandedRequests.fetchAll(false); | |
Array.prototype.push.apply(resps, newResponses.getResponses()); | |
})(responses)); | |
return new BatchResponses(responses); | |
}, | |
add: function (item) { | |
item.muteHttpExceptions = true; | |
_list.push(item); | |
}, | |
length: function (item) { | |
return _list.length; | |
} | |
}; | |
}; | |
/** | |
* Represents a request. | |
* | |
* @constructor | |
* @param {object} [_options={}] - Options object | |
*/ | |
var Request = function (_options) { | |
_options = _options || {}; | |
_options.url = _options.url || config.baseUrl; | |
if (typeof _options.url === 'object' && config.baseUrl) { | |
_options.url = self.format(config.baseUrl, _options.url); | |
} | |
_options.body = _options.body || {}; | |
_options.headers = _options.headers || null; | |
_options.query = _options.query || {}; | |
var toParams; | |
return { | |
params: function (includeUrl) { | |
if (typeof includeUrl === 'undefined') includeUrl = false; | |
var params = {}; | |
params.muteHttpExceptions = true; | |
params.method = _options.method.toLowerCase() || 'get'; | |
if (_options.headers || config.headers) { | |
params.headers = self.utils.extend(true, config.headers, _options.headers); | |
} | |
if ( ['put', 'post'].indexOf(params.method) !== -1 ) { | |
params.payload = self.utils.extend(true, config.body, _options.body); | |
params.payload = JSON.stringify(params.payload); | |
params.contentType = 'application/json'; | |
} | |
if (includeUrl) params.url = this.getUrl(); | |
return params; | |
}, | |
/* | |
Prepares the params parameter of UrlFetchApp.fetch and returns | |
custom response object | |
*/ | |
build: function () { | |
var params, resp, reply; | |
params = this.params(); | |
reply = UrlFetchApp.fetch(this.getUrl(), params); | |
resp = new Response(reply); | |
resp.request = this; | |
resp.raw = UrlFetchApp.getRequest(this.getUrl(), params); | |
return resp; | |
}, | |
/* | |
Fetches external resource, handling any API rate limitations | |
*/ | |
fetch: function () { | |
var resp; | |
resp = this.build(); | |
if (resp.hitRateLimit()) { | |
resp = this.build(); | |
} | |
return resp; | |
}, | |
getUrl: function () { | |
var obj = self.utils.extend(true, config.query, _options.query); | |
if (_options.url.indexOf('?') !== -1) _options.url = _options.url.slice(0, _options.url.indexOf('?')); | |
if (Object.keys(obj).length == 0) return _options.url; | |
var ret = _options.url + "?" + Object.keys(obj).reduce(function(a,k){a.push(k+'='+encodeURIComponent(obj[k]));return a},[]).join('&'); | |
return ret; | |
}, | |
setQuery: function (key, value) { | |
_options.query[key] = value; | |
}, | |
getQuery: function () { | |
return _options.query; | |
}, | |
clearQuery: function () { | |
_options.query = {}; | |
}, | |
toRequestObject: function () { | |
// The last object is required to ensure query params are included | |
return self.utils.extend(true, config, _options, {url: this.getUrl()}); | |
} | |
}; | |
}; | |
return { | |
/* | |
Perform the same method on a pattern-identifying URL | |
*/ | |
batch: function (urlTemplate, items, options) { | |
options = options || {}; | |
options.expandForPages = options.expandForPages || false; | |
var requests, batchRequests; | |
if (typeof urlTemplate === 'object' && config.baseUrl) { | |
urlTemplate = self.format(config.baseUrl, urlTemplate); | |
} | |
requests = items.reduce(function (acc, item) { | |
var url, req, reqObj; | |
if (item.options) { | |
// If the last item is an object, store it in the options area and remove it | |
options = self.utils.extend(options, item.options); | |
delete item[item.length-1]; | |
} | |
url = self.format(urlTemplate, item); | |
options.method = options.method || 'get'; | |
options.url = url; | |
options.muteHttpExceptions = true; | |
req = new Request(options); | |
reqObj = req.toRequestObject(); | |
acc.push(reqObj); | |
return acc; | |
}, []); | |
return new BatchRequests(requests).fetchAll(options.expandForPages); | |
}, | |
get: function (url, options, fetchYN) { | |
var req; | |
if (typeof fetchYN === 'undefined') fetchYN = true; | |
options = options || {}; | |
options.url = url; | |
options.method = 'get'; | |
req = new Request(options); | |
if (fetchYN) return req.fetch(); | |
return req; | |
}, | |
post: function (url, options, fetchYN) { | |
if (typeof fetchYN === 'undefined') fetchYN = true; | |
options = options || {}; | |
options.url = url; | |
options.method = 'post'; | |
req = new Request(options); | |
if (fetchYN) return req.fetch(); | |
return req; | |
}, | |
put: function (url, options, fetchYN) { | |
if (typeof fetchYN === 'undefined') fetchYN = true; | |
options = options || {}; | |
options.url = url; | |
options.method = 'put'; | |
req = new Request(options); | |
if (fetchYN) return req.fetch(); | |
return req; | |
}, | |
delete_: function (url, options, fetchYN) { | |
if (typeof fetchYN === 'undefined') fetchYN = true; | |
options = options || {}; | |
options.url = url; | |
options.method = 'delete'; | |
req = new Request(options); | |
if (fetchYN) return req.fetch(); | |
return req; | |
}, | |
head: function (url, options, fetchYN) { | |
if (typeof fetchYN === 'undefined') fetchYN = true; | |
options = options || {}; | |
options.url = url; | |
options.method = 'head'; | |
req = new Request(options); | |
if (fetchYN) return req.fetch(); | |
return req; | |
}, | |
options: function (url, options, fetchYN) { | |
if (typeof fetchYN === 'undefined') fetchYN = true; | |
options = options || {}; | |
options.url = url; | |
options.method = 'options'; | |
req = new Request(options); | |
if (fetchYN) return req.fetch(); | |
return req; | |
}, | |
}; | |
}, | |
{ /* helpers */ | |
fetchAll: function () { | |
var requestsParams = []; | |
for (var a=0; a < arguments.length; a++) { | |
requestsParams.push(arguments[a].params(true)); | |
} | |
return UrlFetchApp.fetchAll(requestsParams); | |
}, | |
runRequest: function () { | |
var run = this({ | |
oauth: 'me', | |
discovery: { | |
name: 'script', | |
version: 'v1', | |
category: 'scripts', | |
method: 'run' | |
} | |
}); | |
return function () { | |
var func, params, request; | |
func = Array.prototype.slice.call(arguments, 0, 1)[0]; | |
params = Array.prototype.slice.call(arguments, 1); | |
request = run.post({scriptId: ScriptApp.getScriptId()}, { | |
body: { | |
parameters: params, | |
'function': func, | |
devMode: true | |
} | |
}, false); | |
return request; | |
}.apply(null, arguments); | |
}, | |
runner: function () { | |
var request, response; | |
request = this.runRequest.apply(this, arguments); | |
response =request.fetch().json(); | |
if (response.error) { | |
Logger.log(response); | |
throw Error("Cannot run function"); | |
} | |
return response.response.result; | |
}, | |
/* | |
http://www.{name}.com, {name: 'hey'} => http://www.hey.com | |
*/ | |
format: function (template /*, obj */) { | |
// ValueError :: String -> Error | |
var ValueError = function(message) { | |
var err = new Error(message); | |
err.name = 'ValueError'; | |
return err; | |
}; | |
// defaultTo :: a,a? -> a | |
var defaultTo = function(x, y) { | |
return y == null ? x : y; | |
}; | |
var lookup = function(obj, path) { | |
if (!/^\d+$/.test(path[0])) { | |
path = ['0'].concat(path); | |
} | |
for (var idx = 0; idx < path.length; idx += 1) { | |
var key = path[idx]; | |
obj = typeof obj[key] === 'function' ? obj[key]() : obj[key]; | |
} | |
return obj; | |
}; | |
var args = Array.prototype.slice.call(arguments, 1); | |
var idx = 0; | |
var state = 'UNDEFINED'; | |
return template.replace( | |
/([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, | |
function(match, literal, key, xf) { | |
if (literal != null) { | |
return literal; | |
} | |
if (key.length > 0) { | |
if (state === 'IMPLICIT') { | |
throw ValueError('cannot switch from ' + | |
'implicit to explicit numbering'); | |
} | |
state = 'EXPLICIT'; | |
} else { | |
if (state === 'EXPLICIT') { | |
throw ValueError('cannot switch from ' + | |
'explicit to implicit numbering'); | |
} | |
state = 'IMPLICIT'; | |
key = String(idx); | |
idx += 1; | |
} | |
var value = defaultTo('', lookup(args, key.split('.'))); | |
if (xf == null) { | |
return value; | |
} else if (Object.prototype.hasOwnProperty.call(transformers, xf)) { | |
return transformers[xf](value); | |
} else { | |
throw ValueError('no transformer named "' + xf + '"'); | |
} | |
} | |
); | |
}, | |
utils: { | |
/* | |
https://github.com/cferdinandi/extend | |
*/ | |
extend: function () { | |
var extend = function () { | |
// Variables | |
var extended = {}; | |
var deep = false; | |
var i = 0; | |
var length = arguments.length; | |
// Check if a deep merge | |
if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) { | |
deep = arguments[0]; | |
i++; | |
} | |
// Merge the object into the extended object | |
var merge = function ( obj ) { | |
for ( var prop in obj ) { | |
if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) { | |
// If deep merge and property is an object, merge properties | |
if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) { | |
extended[prop] = extend( true, extended[prop], obj[prop] ); | |
} else { | |
extended[prop] = obj[prop]; | |
} | |
} | |
} | |
}; | |
// Loop through each object and conduct a merge | |
for ( ; i < length; i++ ) { | |
var obj = arguments[i]; | |
merge(obj); | |
} | |
return extended; | |
} | |
return extend.apply(extend, arguments); | |
}, | |
/* | |
Flatten list into of rows with objects into list, first row being headers | |
*/ | |
flatten: function (rows, options) { | |
/* | |
Dotize: https://github.com/vardars/dotize/blob/master/src/dotize.js | |
Flatten an object that contains nested objects into an object with just one layer, | |
with keys in dotted notation. | |
*/ | |
var dotize = function(obj, prefix) { | |
var newObj = {}; | |
if ((!obj || typeof obj != "object") && !Array.isArray(obj)) { | |
if (prefix) { | |
newObj[prefix] = obj; | |
return newObj; | |
} else { | |
return obj; | |
} | |
} | |
function isNumber(f) { | |
return !isNaN(parseInt(f)); | |
} | |
function isEmptyObj(obj) { | |
for (var prop in obj) { | |
if (Object.hasOwnProperty.call(obj, prop)) | |
return false; | |
} | |
} | |
function getFieldName(field, prefix, isRoot, isArrayItem, isArray) { | |
if (isArray) | |
return (prefix ? prefix : "") + (isNumber(field) ? "[" + field + "]" : (isRoot ? "" : ".") + field); | |
else if (isArrayItem) | |
return (prefix ? prefix : "") + "[" + field + "]"; | |
else | |
return (prefix ? prefix + "." : "") + field; | |
} | |
return function recurse(o, p, isRoot) { | |
var isArrayItem = Array.isArray(o); | |
for (var f in o) { | |
var currentProp = o[f]; | |
if (currentProp && typeof currentProp === "object") { | |
if (Array.isArray(currentProp)) { | |
newObj = recurse(currentProp, getFieldName(f, p, isRoot, false, true), isArrayItem); // array | |
} else { | |
if (isArrayItem && isEmptyObj(currentProp) == false) { | |
newObj = recurse(currentProp, getFieldName(f, p, isRoot, true)); // array item object | |
} else if (isEmptyObj(currentProp) == false) { | |
newObj = recurse(currentProp, getFieldName(f, p, isRoot)); // object | |
} else { | |
// | |
} | |
} | |
} else { | |
if (isArrayItem || isNumber(f)) { | |
newObj[getFieldName(f, p, isRoot, true)] = currentProp; // array item primitive | |
} else { | |
newObj[getFieldName(f, p, isRoot)] = currentProp; // primitive | |
} | |
} | |
} | |
return newObj; | |
}(obj, prefix, true); | |
}; | |
options = options || {}; | |
options.pathDelimiter = options.pathDelimiter || '.'; | |
var headers; | |
rows = rows.map(function (row) { | |
return dotize(row); | |
}); | |
headers = rows.reduce(function (everyHeader, row) { | |
var prop; | |
for (prop in row) { | |
everyHeader.push( prop ); | |
} | |
return everyHeader; | |
}, []); | |
var mappedHeaders, finalHeaders; | |
mappedHeaders = headers.map(function (el, i) { | |
return { | |
index: i, | |
value: el === 'id' ? '' : el.toLowerCase() | |
} | |
}); | |
mappedHeaders.sort(function (a, b) { | |
if (a.value > b.value) return 1; | |
if (a.value < b.value) return -1; | |
return 0; | |
}); | |
finalHeaders = mappedHeaders.map(function (el) { | |
return headers[el.index]; | |
}).filter(function (value, index, me) { | |
return me.indexOf(value) === index; | |
}); | |
return rows.reduce(function (acc, obj) { | |
var row, value; | |
row = []; | |
for (var h=0; h < finalHeaders.length; h++) { | |
value = obj[finalHeaders[h]]; | |
if (typeof value === 'undefined' || value == null) value = ""; | |
row.push(value); | |
} | |
acc.push(row); | |
return acc; | |
}, [finalHeaders]); | |
} | |
}, | |
} | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment