{"action":"created","comment":{"url":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/comments/64138426","id":64138426,"diff_hunk":"@@ -0,0 +1,358 @@\n+// NOTE: Each of the following should be bound in the global scope:\n+//   - `gapi'     : Google API Javascript Client\n+//   - `clientId' : Google Client ID\n+//   - `apiKey'   : Google API Key\n+//   - `Q'        : Q Promise Framework\n+\n+/**\n+ * The most recently authenticated version of the\n+ * wrapped Google API\n+ */\n+var gwrap = {\n+  // Initialize to a dummy method which loads the wrapper\n+  load: function(params) {\n+    if (!params || !params.reauth || (params.reauth.immediate === undefined)) {\n+      throw new Error(\"Google API Wrapper not yet initialized\");\n+    }\n+    var ret = Q.defer();\n+    loadAPIWrapper(params.reauth.immediate)\n+      .then(function(gw) {\n+        // Shallow copy\n+        var copy = $.extend({}, params);\n+        delete copy.reauth;\n+        gw.load(copy)\n+          .then(function(loaded) {\n+            ret.resolve(loaded);\n+          });\n+      });\n+    return ret.promise;\n+  },\n+  request: function() {\n+    throw new Error(\"Google API Wrapper not yet initialized\");\n+  }\n+};\n+\n+// GAPI Client APIs whose methods have been wrapped with calls to gQ\n+// (This is kept in the global state to avoid unneeded reloading)\n+var _GWRAP_APIS = {};\n+\n+/**\n+ * Base module for interfacing with Google APIs.\n+ * Note that any APIs will only be authenticated\n+ * with the OAuth scopes found in /src/google-auth.js.\n+ * @param {boolean} immediate - See the description of `immediate`\n+ *        on `reauth`.\n+ */\n+function loadAPIWrapper(immediate) {\n+  \n+  // Sanity check: Make sure aforementioned things are\n+  //               actually defined.\n+\n+  /**\n+   * Raises an initialization error if a variable with\n+   * the given name is not defined in the global scope\n+   * @param {string} v - The variable to check\n+   */\n+  function assertDefined(v) {\n+    if (window[v] === undefined) {\n+      throw new Error(\"Cannot initialize wrapper! \" +\n+                      \"Missing required definition: \" + v);\n+    }\n+  }\n+  assertDefined('gapi');\n+  assertDefined('clientId');\n+  assertDefined('apiKey');\n+  assertDefined('Q');\n+  // Sanity check passed.\n+\n+  // Custom error type definitions\n+  // http://stackoverflow.com/questions/1382107/whats-a-good-way-to-extend-error-in-javascript\n+  /**\n+   * Creates a new Google API Error\n+   * @constructor\n+   * @param [message] - The error message for this error\n+   */\n+  function GoogleAPIError(message) {\n+    Error.captureStackTrace(this, this.constructor);\n+    this.name = this.constructor.name;\n+    this.message = message || \"\";\n+  }\n+\n+  GoogleAPIError.prototype = Object.create(Error.prototype);\n+  GoogleAPIError.prototype.name = \"GoogleAPIError\";\n+  GoogleAPIError.prototype.constructor = GoogleAPIError;\n+\n+  /**\n+   * Alias for {@link GoogleAPIError}.\n+   * @see GoogleAPIError\n+   */\n+  var GAPIError = GoogleAPIError;\n+\n+  /**\n+   * Creates a new Authentication Error. These are caused by\n+   * insufficient permissions.\n+   * @constructor\n+   * @see GoogleAPIError\n+   * @param [message] - The error message for this error\n+   */\n+  function AuthenticationError(message) {\n+    this.message = message || \"\";\n+  }\n+  AuthenticationError.prototype = Object.create(GoogleAPIError.prototype);\n+  AuthenticationError.prototype.name = \"AuthenticationError\";\n+  AuthenticationError.prototype.constructor = AuthenticationError;\n+\n+  /**\n+   * Creates a new API Response Error. These are caused by invalid/error\n+   * responses from the Google API; e.g. 404 Not Found errors\n+   * @constructor\n+   * @see GoogleAPIError\n+   * @param [message] - The error message for this error\n+   */\n+  function APIResponseError(message) {\n+    this.message = message || \"\";\n+  }\n+  APIResponseError.prototype = Object.create(GoogleAPIError.prototype);\n+  APIResponseError.prototype.name = \"APIResponseError\";\n+  APIResponseError.prototype.constructor = APIResponseError;\n+\n+  // Function definitions\n+\n+  /**\n+   * Reauthenticates the current session.\n+   * @param {boolean} immediate - Whether the user needs to log in with\n+   *        Google (if false, the user will receive a refreshed access token).\n+   * @returns A promise which will resolve following the re-authentication.\n+   */\n+  function reauth(immediate) {\n+    var d = Q.defer();\n+    if (!immediate) {\n+      // Need to do a login to get a cookie for this user; do it in a popup\n+      var w = window.open(\"/login?redirect=\" + encodeURIComponent(\"/close.html\"));\n+      window.addEventListener('message', function(e) {\n+        if (e.domain === document.location.origin) {\n+          d.resolve(reauth(true));\n+        } else {\n+          d.resolve(null);\n+        }\n+      });\n+    } else {\n+      // The user is logged in, but needs an access token from our server\n+      var newToken = $.ajax(\"/getAccessToken\", { method: \"get\", datatype: \"json\" });\n+      newToken.then(function(t) {\n+        gapi.auth.setToken({ access_token: t.access_token });\n+        d.resolve({ access_token: t.access_token });\n+      });\n+      newToken.fail(function(t) {\n+        d.resolve(null);\n+      });\n+    }\n+    return d.promise;\n+  }\n+\n+  /**\n+   * Reauthenticates the current session with Google.\n+   * @see reauth\n+   * @param {boolean} immediate - DEPRECATED: UNUSED\n+   */\n+  function refresh(immediate) {\n+    if (arguments.length > 0) {\n+      console.warn(\"The `immediate` argument to `refresh()` is deprecated.\");\n+    }\n+    return reauth(true);\n+  }\n+\n+  /**\n+   * Runs the given thunk. If running it results in an\n+   * authentication failure, the session is re-authorized and the\n+   * function is run again.\n+   * @param {function} f - The thunk to run. It should return a promise.\n+   * @returns The given promise plus any needed re-authentication\n+   */\n+  function authCheck(f) {\n+    function isAuthFailure(result) {\n+      return result\n+        && ((result.error && result.error.code && result.error.code === 401)\n+            || (result.code && result.code === 401));\n+    }\n+    var retry = f().then(function(result) {\n+      if(isAuthFailure(result)) {\n+        return refresh().then(function(authResult) {\n+          if(!authResult || authResult.error) {\n+            return { error: { code: 401, message: \"Couldn't re-authorize\" } };\n+          } else {\n+            return f();\n+          }\n+        });\n+      } else {\n+        return result;\n+      }\n+    });\n+    return retry.then(function(result) {\n+      if(isAuthFailure(result)) {\n+        throw new AuthenticationError(\"Authentication failure\");\n+      }\n+      return result;\n+    });\n+  }\n+\n+  /**\n+   * Wraps the given promise with a check for a failed\n+   * API response.\n+   * @param {Promise} p - The promise to wrap\n+   * @returns {Promise} The wrapped promise\n+   */\n+  function failCheck(p) {\n+    return p.then(function(result) {\n+      // Network error\n+      if (result && result.error) {\n+        console.error(\"Error getting Google API response: \", result);\n+        throw new APIResponseError(result);\n+      }\n+      if (result && (typeof result.code === \"number\") && (result.code >= 400)) {\n+        console.error(\"40X Error getting Google API response: \", result);\n+        throw new APIResponseError(result);\n+      }\n+      return result;\n+    });\n+  }\n+\n+  /**\n+   * Wraps the given Google query in a promise\n+   * @param request - The request object to wrap (made by methods from `gapi')\n+   * @param {boolean} [skipAuth] - If true, performs the request without\n+   *        authentication.\n+   * @returns {Promise} A promise which resolves to the result of the Google query\n+   */\n+  function gQ(request, skipAuth) {\n+    var oldAccess = gapi.auth.getToken();\n+    if (skipAuth) { gapi.auth.setToken({ access_token: null }); }\n+    var ret = failCheck(authCheck(function() {\n+      var d = Q.defer();\n+      // TODO: This should be migrated to a promise\n+      request.execute(function(result) {\n+        d.resolve(result);\n+      });\n+      return d.promise;\n+    }));\n+    if (skipAuth) {\n+      ret.fin(function() {\n+        gapi.auth.setToken({ access_token: oldAccess });\n+      });\n+    }\n+    return ret;\n+  }\n+\n+  \n+\n+  /**\n+   * Loads the Google API denoted by the given parameters\n+   * @param {object} params - The designator for the API. Valid fields\n+            are `name`, `url`, `version`, and `callback`\n+   * @returns Nothing if `params.callback` is provided, otherwise a promise\n+   *          which resolves to the loaded API/APIs.\n+   */\n+  function loadAPI(params) {\n+    if (!params) {\n+      throw new GoogleAPIError(\"Missing API loading parameters\");\n+    }\n+    if (params.reauth) {\n+      if (params.reauth.immediate === undefined) {\n+        throw new GoogleAPIError(\"Missing required field to load(): \"\n+                                 + \"params.reauth.immediate\");\n+      }\n+      var reloaded = Q.defer();\n+      loadAPIWrapper(params.reauth.immediate)\n+        .then(function(gw) {\n+          // Shallow copy\n+          var copy = $.extend({}, params);\n+          delete copy.reauth;\n+          gw.load(copy)\n+            .then(function(loaded) {\n+              reloaded.resolve(loaded);\n+            });\n+        });\n+      return reloaded.promise;\n+    }\n+    var preKeys = Object.keys(gapi.client);\n+\n+    function processKey(key) {\n+      function processObject(obj, dest) {\n+        Object.keys(obj).forEach(function(key) {\n+          if (typeof(obj[key]) === \"object\") {\n+            dest[key] = {};\n+            processObject(obj[key], dest[key]);\n+          } else if (typeof(obj[key]) === \"function\") {\n+            // Use one argument, since it seems that's all that's\n+            // used, and there's no `Function.prototype.apply` equivalent\n+            // that doesn't change the function's context\n+            var original = obj[key];\n+            dest[key] = (function(args, skipAuth) {\n+              return gQ(original(args), skipAuth);\n+            });\n+          } else {\n+            dest[key] = obj[key];\n+          }\n+        });\n+      }\n+\n+      if (Object.keys(_GWRAP_APIS).includes(key)) {\n+        // Do nothing\n+        return null;","path":"src/web/js/google-apis/api-wrapper.js","position":null,"original_position":301,"commit_id":"2fc03c4557afd5c7a237b8c0928a7d1e155b170c","original_commit_id":"25097bfa64db34cc80ef6761c8a251c92e373c29","user":{"login":"blerner","id":918464,"avatar_url":"https://avatars.githubusercontent.com/u/918464?v=3","gravatar_id":"","url":"https://api.github.com/users/blerner","html_url":"https://github.com/blerner","followers_url":"https://api.github.com/users/blerner/followers","following_url":"https://api.github.com/users/blerner/following{/other_user}","gists_url":"https://api.github.com/users/blerner/gists{/gist_id}","starred_url":"https://api.github.com/users/blerner/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/blerner/subscriptions","organizations_url":"https://api.github.com/users/blerner/orgs","repos_url":"https://api.github.com/users/blerner/repos","events_url":"https://api.github.com/users/blerner/events{/privacy}","received_events_url":"https://api.github.com/users/blerner/received_events","type":"User","site_admin":false},"body":"Oh, I think I missed the distinction between `processKey` and `processObject` (because the latter function is nested in the former, I kinda missed it).","created_at":"2016-05-21T18:42:26Z","updated_at":"2016-05-21T18:42:26Z","html_url":"https://github.com/brownplt/code.pyret.org/pull/72#discussion_r64138426","pull_request_url":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/72","_links":{"self":{"href":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/comments/64138426"},"html":{"href":"https://github.com/brownplt/code.pyret.org/pull/72#discussion_r64138426"},"pull_request":{"href":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/72"}}},"pull_request":{"url":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/72","id":70952363,"html_url":"https://github.com/brownplt/code.pyret.org/pull/72","diff_url":"https://github.com/brownplt/code.pyret.org/pull/72.diff","patch_url":"https://github.com/brownplt/code.pyret.org/pull/72.patch","issue_url":"https://api.github.com/repos/brownplt/code.pyret.org/issues/72","number":72,"state":"open","locked":false,"title":"Re-engineered the Google API infrastructure to be more extensible with new APIs","user":{"login":"belph","id":4998607,"avatar_url":"https://avatars.githubusercontent.com/u/4998607?v=3","gravatar_id":"","url":"https://api.github.com/users/belph","html_url":"https://github.com/belph","followers_url":"https://api.github.com/users/belph/followers","following_url":"https://api.github.com/users/belph/following{/other_user}","gists_url":"https://api.github.com/users/belph/gists{/gist_id}","starred_url":"https://api.github.com/users/belph/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/belph/subscriptions","organizations_url":"https://api.github.com/users/belph/orgs","repos_url":"https://api.github.com/users/belph/repos","events_url":"https://api.github.com/users/belph/events{/privacy}","received_events_url":"https://api.github.com/users/belph/received_events","type":"User","site_admin":false},"body":"Essentially, this adds a layer between the Drive API (what was in `programs.js`) and the `gapi` object. This layer provides the same loading/API access while abstracting away things like making sure the session is still authenticated with our infrastructure.\r\n\r\nThis has the effect of removing the need to wrap each API call with a call to `gQ`, along with the benefit of pulling out the boilerplate code needed for new API integrations.\r\n\r\nFor example, I would like to use a similar infrastructure to interface with the new sheets API, but doing so requires the reintroduction of a significant amount of code. On the other hand, this provides the needed abstractions to avoid a large amount of this rewriting.","created_at":"2016-05-21T15:32:45Z","updated_at":"2016-05-21T18:42:26Z","closed_at":null,"merged_at":null,"merge_commit_sha":"e6d446b619514e9ba64b9d7169ca45dc0184e702","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/72/commits","review_comments_url":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/72/comments","review_comment_url":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/comments{/number}","comments_url":"https://api.github.com/repos/brownplt/code.pyret.org/issues/72/comments","statuses_url":"https://api.github.com/repos/brownplt/code.pyret.org/statuses/2fc03c4557afd5c7a237b8c0928a7d1e155b170c","head":{"label":"brownplt:gapi-wrapper","ref":"gapi-wrapper","sha":"2fc03c4557afd5c7a237b8c0928a7d1e155b170c","user":{"login":"brownplt","id":391839,"avatar_url":"https://avatars.githubusercontent.com/u/391839?v=3","gravatar_id":"","url":"https://api.github.com/users/brownplt","html_url":"https://github.com/brownplt","followers_url":"https://api.github.com/users/brownplt/followers","following_url":"https://api.github.com/users/brownplt/following{/other_user}","gists_url":"https://api.github.com/users/brownplt/gists{/gist_id}","starred_url":"https://api.github.com/users/brownplt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brownplt/subscriptions","organizations_url":"https://api.github.com/users/brownplt/orgs","repos_url":"https://api.github.com/users/brownplt/repos","events_url":"https://api.github.com/users/brownplt/events{/privacy}","received_events_url":"https://api.github.com/users/brownplt/received_events","type":"Organization","site_admin":false},"repo":{"id":19346887,"name":"code.pyret.org","full_name":"brownplt/code.pyret.org","owner":{"login":"brownplt","id":391839,"avatar_url":"https://avatars.githubusercontent.com/u/391839?v=3","gravatar_id":"","url":"https://api.github.com/users/brownplt","html_url":"https://github.com/brownplt","followers_url":"https://api.github.com/users/brownplt/followers","following_url":"https://api.github.com/users/brownplt/following{/other_user}","gists_url":"https://api.github.com/users/brownplt/gists{/gist_id}","starred_url":"https://api.github.com/users/brownplt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brownplt/subscriptions","organizations_url":"https://api.github.com/users/brownplt/orgs","repos_url":"https://api.github.com/users/brownplt/repos","events_url":"https://api.github.com/users/brownplt/events{/privacy}","received_events_url":"https://api.github.com/users/brownplt/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/brownplt/code.pyret.org","description":"Website for serving Pyret to folks.","fork":false,"url":"https://api.github.com/repos/brownplt/code.pyret.org","forks_url":"https://api.github.com/repos/brownplt/code.pyret.org/forks","keys_url":"https://api.github.com/repos/brownplt/code.pyret.org/keys{/key_id}","collaborators_url":"https://api.github.com/repos/brownplt/code.pyret.org/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/brownplt/code.pyret.org/teams","hooks_url":"https://api.github.com/repos/brownplt/code.pyret.org/hooks","issue_events_url":"https://api.github.com/repos/brownplt/code.pyret.org/issues/events{/number}","events_url":"https://api.github.com/repos/brownplt/code.pyret.org/events","assignees_url":"https://api.github.com/repos/brownplt/code.pyret.org/assignees{/user}","branches_url":"https://api.github.com/repos/brownplt/code.pyret.org/branches{/branch}","tags_url":"https://api.github.com/repos/brownplt/code.pyret.org/tags","blobs_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/refs{/sha}","trees_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/trees{/sha}","statuses_url":"https://api.github.com/repos/brownplt/code.pyret.org/statuses/{sha}","languages_url":"https://api.github.com/repos/brownplt/code.pyret.org/languages","stargazers_url":"https://api.github.com/repos/brownplt/code.pyret.org/stargazers","contributors_url":"https://api.github.com/repos/brownplt/code.pyret.org/contributors","subscribers_url":"https://api.github.com/repos/brownplt/code.pyret.org/subscribers","subscription_url":"https://api.github.com/repos/brownplt/code.pyret.org/subscription","commits_url":"https://api.github.com/repos/brownplt/code.pyret.org/commits{/sha}","git_commits_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/commits{/sha}","comments_url":"https://api.github.com/repos/brownplt/code.pyret.org/comments{/number}","issue_comment_url":"https://api.github.com/repos/brownplt/code.pyret.org/issues/comments{/number}","contents_url":"https://api.github.com/repos/brownplt/code.pyret.org/contents/{+path}","compare_url":"https://api.github.com/repos/brownplt/code.pyret.org/compare/{base}...{head}","merges_url":"https://api.github.com/repos/brownplt/code.pyret.org/merges","archive_url":"https://api.github.com/repos/brownplt/code.pyret.org/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/brownplt/code.pyret.org/downloads","issues_url":"https://api.github.com/repos/brownplt/code.pyret.org/issues{/number}","pulls_url":"https://api.github.com/repos/brownplt/code.pyret.org/pulls{/number}","milestones_url":"https://api.github.com/repos/brownplt/code.pyret.org/milestones{/number}","notifications_url":"https://api.github.com/repos/brownplt/code.pyret.org/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/brownplt/code.pyret.org/labels{/name}","releases_url":"https://api.github.com/repos/brownplt/code.pyret.org/releases{/id}","deployments_url":"https://api.github.com/repos/brownplt/code.pyret.org/deployments","created_at":"2014-05-01T14:33:23Z","updated_at":"2016-03-01T15:42:37Z","pushed_at":"2016-05-21T18:36:31Z","git_url":"git://github.com/brownplt/code.pyret.org.git","ssh_url":"git@github.com:brownplt/code.pyret.org.git","clone_url":"https://github.com/brownplt/code.pyret.org.git","svn_url":"https://github.com/brownplt/code.pyret.org","homepage":null,"size":1728,"stargazers_count":1,"watchers_count":1,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":9,"mirror_url":null,"open_issues_count":40,"forks":9,"open_issues":40,"watchers":1,"default_branch":"master"}},"base":{"label":"brownplt:horizon","ref":"horizon","sha":"e74eb0e33b5b37909e285ad48742c5009d977410","user":{"login":"brownplt","id":391839,"avatar_url":"https://avatars.githubusercontent.com/u/391839?v=3","gravatar_id":"","url":"https://api.github.com/users/brownplt","html_url":"https://github.com/brownplt","followers_url":"https://api.github.com/users/brownplt/followers","following_url":"https://api.github.com/users/brownplt/following{/other_user}","gists_url":"https://api.github.com/users/brownplt/gists{/gist_id}","starred_url":"https://api.github.com/users/brownplt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brownplt/subscriptions","organizations_url":"https://api.github.com/users/brownplt/orgs","repos_url":"https://api.github.com/users/brownplt/repos","events_url":"https://api.github.com/users/brownplt/events{/privacy}","received_events_url":"https://api.github.com/users/brownplt/received_events","type":"Organization","site_admin":false},"repo":{"id":19346887,"name":"code.pyret.org","full_name":"brownplt/code.pyret.org","owner":{"login":"brownplt","id":391839,"avatar_url":"https://avatars.githubusercontent.com/u/391839?v=3","gravatar_id":"","url":"https://api.github.com/users/brownplt","html_url":"https://github.com/brownplt","followers_url":"https://api.github.com/users/brownplt/followers","following_url":"https://api.github.com/users/brownplt/following{/other_user}","gists_url":"https://api.github.com/users/brownplt/gists{/gist_id}","starred_url":"https://api.github.com/users/brownplt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brownplt/subscriptions","organizations_url":"https://api.github.com/users/brownplt/orgs","repos_url":"https://api.github.com/users/brownplt/repos","events_url":"https://api.github.com/users/brownplt/events{/privacy}","received_events_url":"https://api.github.com/users/brownplt/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/brownplt/code.pyret.org","description":"Website for serving Pyret to folks.","fork":false,"url":"https://api.github.com/repos/brownplt/code.pyret.org","forks_url":"https://api.github.com/repos/brownplt/code.pyret.org/forks","keys_url":"https://api.github.com/repos/brownplt/code.pyret.org/keys{/key_id}","collaborators_url":"https://api.github.com/repos/brownplt/code.pyret.org/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/brownplt/code.pyret.org/teams","hooks_url":"https://api.github.com/repos/brownplt/code.pyret.org/hooks","issue_events_url":"https://api.github.com/repos/brownplt/code.pyret.org/issues/events{/number}","events_url":"https://api.github.com/repos/brownplt/code.pyret.org/events","assignees_url":"https://api.github.com/repos/brownplt/code.pyret.org/assignees{/user}","branches_url":"https://api.github.com/repos/brownplt/code.pyret.org/branches{/branch}","tags_url":"https://api.github.com/repos/brownplt/code.pyret.org/tags","blobs_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/refs{/sha}","trees_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/trees{/sha}","statuses_url":"https://api.github.com/repos/brownplt/code.pyret.org/statuses/{sha}","languages_url":"https://api.github.com/repos/brownplt/code.pyret.org/languages","stargazers_url":"https://api.github.com/repos/brownplt/code.pyret.org/stargazers","contributors_url":"https://api.github.com/repos/brownplt/code.pyret.org/contributors","subscribers_url":"https://api.github.com/repos/brownplt/code.pyret.org/subscribers","subscription_url":"https://api.github.com/repos/brownplt/code.pyret.org/subscription","commits_url":"https://api.github.com/repos/brownplt/code.pyret.org/commits{/sha}","git_commits_url":"https://api.github.com/repos/brownplt/code.pyret.org/git/commits{/sha}","comments_url":"https://api.github.com/repos/brownplt/code.pyret.org/comments{/number}","issue_comment_url":"https://api.github.com/repos/brownplt/code.pyret.org/issues/comments{/number}","contents_url":"https://api.github.com/repos/brownplt/code.pyret.org/contents/{+path}","compare_url":"https://api.github.com/repos/brownplt/code.pyret.org/compare/{base}...{head}","merges_url":"https://api.github.com/repos/brownplt/code.pyret.org/merges","archive_url":"https://api.github.com/repos/brownplt/code.pyret.org/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/brownplt/code.pyret.org/downloads","issues_url":"https://api.github.com/repos/brownplt/code.pyret.org/issues{/number}","pulls_url":"https://api.github.com/repos/brownplt/code.pyret.org/pulls{/number}","milestones_url":"https://api.github.com/repos/brownplt/code.pyret.org/milestones{/number}","notifications_url":"https://api.github.com/repos/brownplt/code.pyret.org/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/brownplt/code.pyret.org/labels{/name}","releases_url":"https://api.github.com/repos/brownplt/code.pyret.org/releases{/id}","deployments_url":"https://api.github.com/repos/brownplt/code.pyret.org/deployments","created_at":"2014-05-01T14:33:23Z","updated_at":"2016-03-01T15:42:37Z","pushed_at":"2016-05-21T18:36:31Z","git_url":"git://github.com/brownplt/code.pyret.org.git","ssh_url":"git@github.com:brownplt/code.pyret.org.git","clone_url":"https://github.com/brownplt/code.pyret.org.git","svn_url":"https://github.com/brownplt/code.pyret.org","homepage":null,"size":1728,"stargazers_count":1,"watchers_count":1,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":9,"mirror_url":null,"open_issues_count":40,"forks":9,"open_issues":40,"watchers":1,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/72"},"html":{"href":"https://github.com/brownplt/code.pyret.org/pull/72"},"issue":{"href":"https://api.github.com/repos/brownplt/code.pyret.org/issues/72"},"comments":{"href":"https://api.github.com/repos/brownplt/code.pyret.org/issues/72/comments"},"review_comments":{"href":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/72/comments"},"review_comment":{"href":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/brownplt/code.pyret.org/pulls/72/commits"},"statuses":{"href":"https://api.github.com/repos/brownplt/code.pyret.org/statuses/2fc03c4557afd5c7a237b8c0928a7d1e155b170c"}}}}
{"actor":{"display_login":"blerner"}}