{"action":"created","comment":{"url":"https://api.github.com/repos/mozilla/balrog/pulls/comments/100819891","pull_request_review_id":21535459,"id":100819891,"diff_hunk":"@@ -0,0 +1,272 @@\n+import json\n+\n+from sqlalchemy.sql.expression import null\n+\n+from flask import jsonify, Response, request\n+\n+from auslib.admin.views.base import requirelogin, AdminView, HistoryAdminView\n+from auslib.admin.views.forms import ProductRequiredSignoffForm, \\\n+    ProductRequiredSignoffHistoryForm, \\\n+    ScheduledChangeExistingProductRequiredSignoffForm, \\\n+    ScheduledChangeNewProductRequiredSignoffForm, \\\n+    ScheduledChangeDeleteProductRequiredSignoffForm, \\\n+    EditScheduledChangeNewProductRequiredSignoffForm, \\\n+    EditScheduledChangeExistingProductRequiredSignoffForm, \\\n+    PermissionsRequiredSignoffForm, \\\n+    PermissionsRequiredSignoffHistoryForm, \\\n+    ScheduledChangeExistingPermissionsRequiredSignoffForm, \\\n+    ScheduledChangeNewPermissionsRequiredSignoffForm, \\\n+    ScheduledChangeDeletePermissionsRequiredSignoffForm, \\\n+    EditScheduledChangeNewPermissionsRequiredSignoffForm, \\\n+    EditScheduledChangeExistingPermissionsRequiredSignoffForm\n+from auslib.admin.views.scheduled_changes import ScheduledChangesView, \\\n+    ScheduledChangeView, EnactScheduledChangeView, SignoffsView, \\\n+    ScheduledChangeHistoryView\n+from auslib.db import SignoffRequiredError\n+from auslib.global_state import dbo\n+\n+\n+class RequiredSignoffsView(AdminView):\n+\n+    def __init__(self, table, decisionFields):\n+        self.table = table\n+        self.decisionFields = decisionFields\n+        super(RequiredSignoffsView, self).__init__()\n+\n+    def get(self):\n+        rows = self.table.select()\n+        return jsonify({\"count\": len(rows), \"required_signoffs\": [dict(rs) for rs in rows]})\n+\n+    def _post(self, form, transaction, changed_by):\n+        if not form.validate():\n+            self.log.warning(\"Bad input: %s\", form.errors)\n+            return Response(status=400, response=json.dumps(form.errors))\n+\n+        where = {f: form[f].data for f in self.decisionFields}\n+        if self.table.select(where=where, transaction=transaction):\n+            raise SignoffRequiredError(\"Required Signoffs cannot be directly modified\")\n+        else:\n+            try:\n+                self.table.insert(changed_by=changed_by, transaction=transaction, **form.data)\n+                return Response(status=201, response=json.dumps({\"new_data_version\": 1}))\n+            except ValueError as e:\n+                self.log.warning(\"Bad input: %s\", e.args)\n+                return Response(status=400, response=e.args)\n+\n+    def _delete(self, *args, **kwargs):\n+        raise SignoffRequiredError(\"Required Signoffs cannot be directly deleted.\")\n+\n+\n+class RequiredSignoffsHistoryAPIView(HistoryAdminView):\n+\n+    def __init__(self, table, decisionFields):\n+        self.table = table\n+        self.decisionFields = decisionFields\n+        super(RequiredSignoffsHistoryAPIView, self).__init__()\n+\n+    def get(self, form):\n+        if not self.table.select({f: getattr(form, f).data for f in self.decisionFields}):\n+            return Response(status=404, response=\"Requested Required Signoff does not exist\")\n+\n+        try:\n+            page = int(request.args.get('page', 1))\n+            limit = int(request.args.get('limit', 100))\n+            assert page >= 1\n+        except (ValueError, AssertionError) as msg:\n+            self.log.warning(\"Bad input: %s\", msg)\n+            return Response(status=400, response=str(msg))\n+        offset = limit * (page - 1)\n+\n+        query = self.table.history.t.count().where(self.table.history.data_version != null())\n+        for field in self.decisionFields:\n+            query = query.where(getattr(self.table.history, field) == getattr(form, field).data)\n+        total_count = query.execute().fetchone()[0]\n+\n+        where = [getattr(self.table.history, f) == getattr(form, f).data for f in self.decisionFields]\n+        where.append(self.table.history.data_version != null())\n+        revisions = self.table.history.select(\n+            where=where, limit=limit, offset=offset,\n+            order_by=[self.table.history.timestamp.desc()]\n+        )\n+\n+        return jsonify(count=total_count, required_signoffs=revisions)\n+\n+\n+class ProductRequiredSignoffsView(RequiredSignoffsView):\n+\n+    def __init__(self):\n+        super(ProductRequiredSignoffsView, self).__init__(dbo.productRequiredSignoffs, [\"product\", \"channel\", \"role\"])\n+\n+    @requirelogin\n+    def _post(self, transaction, changed_by):\n+        form = ProductRequiredSignoffForm()\n+        return super(ProductRequiredSignoffsView, self)._post(form, transaction, changed_by)\n+\n+\n+class ProductRequiredSignoffsHistoryAPIView(RequiredSignoffsHistoryAPIView):\n+\n+    def __init__(self):\n+        super(ProductRequiredSignoffsHistoryAPIView, self).__init__(dbo.productRequiredSignoffs, [\"product\", \"channel\", \"role\"])\n+\n+    def get(self):\n+        form = ProductRequiredSignoffHistoryForm(request.args)\n+        return super(ProductRequiredSignoffsHistoryAPIView, self).get(form)\n+\n+\n+class ProductRequiredSignoffsScheduledChangesView(ScheduledChangesView):\n+    def __init__(self):\n+        super(ProductRequiredSignoffsScheduledChangesView, self).__init__(\"product_req_signoffs\", dbo.productRequiredSignoffs)\n+\n+    @requirelogin\n+    def _post(self, transaction, changed_by):\n+        change_type = request.json.get(\"change_type\")\n+\n+        if change_type == \"update\":\n+            form = ScheduledChangeExistingProductRequiredSignoffForm()\n+        elif change_type == \"insert\":\n+            form = ScheduledChangeNewProductRequiredSignoffForm()\n+        elif change_type == \"delete\":\n+            form = ScheduledChangeDeleteProductRequiredSignoffForm()\n+        else:\n+            return Response(status=400, response=\"Invalid or missing change_type\")\n+\n+        return super(ProductRequiredSignoffsScheduledChangesView, self)._post(form, transaction, changed_by)\n+\n+\n+class ProductRequiredSignoffScheduledChangeView(ScheduledChangeView):\n+    def __init__(self):\n+        super(ProductRequiredSignoffScheduledChangeView, self).__init__(\"product_req_signoffs\", dbo.productRequiredSignoffs)\n+\n+    @requirelogin\n+    def _post(self, sc_id, transaction, changed_by):\n+        if request.json and request.json.get(\"data_version\"):\n+            form = EditScheduledChangeExistingProductRequiredSignoffForm()\n+        else:\n+            form = EditScheduledChangeNewProductRequiredSignoffForm()\n+\n+        return super(ProductRequiredSignoffScheduledChangeView, self)._post(sc_id, form, transaction, changed_by)\n+\n+    @requirelogin\n+    def _delete(self, sc_id, transaction, changed_by):\n+        return super(ProductRequiredSignoffScheduledChangeView, self)._delete(sc_id, transaction, changed_by)\n+\n+\n+class EnactProductRequiredSignoffScheduledChangeView(EnactScheduledChangeView):\n+    def __init__(self):\n+        super(EnactProductRequiredSignoffScheduledChangeView, self).__init__(\"product_req_signoffs\", dbo.productRequiredSignoffs)\n+\n+    @requirelogin\n+    def _post(self, sc_id, transaction, changed_by):\n+        return super(EnactProductRequiredSignoffScheduledChangeView, self)._post(sc_id, transaction, changed_by)\n+\n+\n+class ProductRequiredSignoffScheduledChangeSignoffsView(SignoffsView):\n+    def __init__(self):\n+        super(ProductRequiredSignoffScheduledChangeSignoffsView, self).__init__(\"product_req_signoffs\", dbo.productRequiredSignoffs)\n+\n+    @requirelogin\n+    def _post(self, sc_id, transaction, changed_by):","path":"auslib/admin/views/required_signoffs.py","position":168,"original_position":168,"commit_id":"4f45c986daedcec54fa341a689d53ec3bf09e76b","original_commit_id":"4f45c986daedcec54fa341a689d53ec3bf09e76b","user":{"login":"mozbhearsum","id":49649,"avatar_url":"https://avatars.githubusercontent.com/u/49649?v=3","gravatar_id":"","url":"https://api.github.com/users/mozbhearsum","html_url":"https://github.com/mozbhearsum","followers_url":"https://api.github.com/users/mozbhearsum/followers","following_url":"https://api.github.com/users/mozbhearsum/following{/other_user}","gists_url":"https://api.github.com/users/mozbhearsum/gists{/gist_id}","starred_url":"https://api.github.com/users/mozbhearsum/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozbhearsum/subscriptions","organizations_url":"https://api.github.com/users/mozbhearsum/orgs","repos_url":"https://api.github.com/users/mozbhearsum/repos","events_url":"https://api.github.com/users/mozbhearsum/events{/privacy}","received_events_url":"https://api.github.com/users/mozbhearsum/received_events","type":"User","site_admin":false},"body":"I think you're probably right that I could move requirelogin to the base class - I'll give it a try.","created_at":"2017-02-13T15:33:59Z","updated_at":"2017-02-13T15:33:59Z","html_url":"https://github.com/mozilla/balrog/pull/218#discussion_r100819891","pull_request_url":"https://api.github.com/repos/mozilla/balrog/pulls/218","_links":{"self":{"href":"https://api.github.com/repos/mozilla/balrog/pulls/comments/100819891"},"html":{"href":"https://github.com/mozilla/balrog/pull/218#discussion_r100819891"},"pull_request":{"href":"https://api.github.com/repos/mozilla/balrog/pulls/218"}}},"pull_request":{"url":"https://api.github.com/repos/mozilla/balrog/pulls/218","id":102946344,"html_url":"https://github.com/mozilla/balrog/pull/218","diff_url":"https://github.com/mozilla/balrog/pull/218.diff","patch_url":"https://github.com/mozilla/balrog/pull/218.patch","issue_url":"https://api.github.com/repos/mozilla/balrog/issues/218","number":218,"state":"open","locked":false,"title":"Web APIs for Required Signoffs","user":{"login":"mozbhearsum","id":49649,"avatar_url":"https://avatars.githubusercontent.com/u/49649?v=3","gravatar_id":"","url":"https://api.github.com/users/mozbhearsum","html_url":"https://github.com/mozbhearsum","followers_url":"https://api.github.com/users/mozbhearsum/followers","following_url":"https://api.github.com/users/mozbhearsum/following{/other_user}","gists_url":"https://api.github.com/users/mozbhearsum/gists{/gist_id}","starred_url":"https://api.github.com/users/mozbhearsum/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozbhearsum/subscriptions","organizations_url":"https://api.github.com/users/mozbhearsum/orgs","repos_url":"https://api.github.com/users/mozbhearsum/repos","events_url":"https://api.github.com/users/mozbhearsum/events{/privacy}","received_events_url":"https://api.github.com/users/mozbhearsum/received_events","type":"User","site_admin":false},"body":"Because we have two nearly identical objects, I took a similar approach to Scheduled Changes and used a base class to implement the logic for these. Other than that, I think much of this work is pretty boilerplate-y. I'm not terribly happy with all of the duplication in the Forms, but each has its own subtle differences, and it gets to be a mess when trying to use shared base classes. Swagger may help here, because it should let us more easily separate field definition, fields-for-a-given-endpoint, and optional vs required.\r\n\r\nI realize these are a little bit difficult to review without UI, but I'm hoping to get initial feedback while I continue working on the UI.","created_at":"2017-01-24T15:55:02Z","updated_at":"2017-02-13T15:33:59Z","closed_at":null,"merged_at":null,"merge_commit_sha":"19c0ec0ea4846da8b536343624307db6ee08ed61","assignee":{"login":"mozbhearsum","id":49649,"avatar_url":"https://avatars.githubusercontent.com/u/49649?v=3","gravatar_id":"","url":"https://api.github.com/users/mozbhearsum","html_url":"https://github.com/mozbhearsum","followers_url":"https://api.github.com/users/mozbhearsum/followers","following_url":"https://api.github.com/users/mozbhearsum/following{/other_user}","gists_url":"https://api.github.com/users/mozbhearsum/gists{/gist_id}","starred_url":"https://api.github.com/users/mozbhearsum/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozbhearsum/subscriptions","organizations_url":"https://api.github.com/users/mozbhearsum/orgs","repos_url":"https://api.github.com/users/mozbhearsum/repos","events_url":"https://api.github.com/users/mozbhearsum/events{/privacy}","received_events_url":"https://api.github.com/users/mozbhearsum/received_events","type":"User","site_admin":false},"assignees":[{"login":"mozbhearsum","id":49649,"avatar_url":"https://avatars.githubusercontent.com/u/49649?v=3","gravatar_id":"","url":"https://api.github.com/users/mozbhearsum","html_url":"https://github.com/mozbhearsum","followers_url":"https://api.github.com/users/mozbhearsum/followers","following_url":"https://api.github.com/users/mozbhearsum/following{/other_user}","gists_url":"https://api.github.com/users/mozbhearsum/gists{/gist_id}","starred_url":"https://api.github.com/users/mozbhearsum/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozbhearsum/subscriptions","organizations_url":"https://api.github.com/users/mozbhearsum/orgs","repos_url":"https://api.github.com/users/mozbhearsum/repos","events_url":"https://api.github.com/users/mozbhearsum/events{/privacy}","received_events_url":"https://api.github.com/users/mozbhearsum/received_events","type":"User","site_admin":false}],"milestone":null,"commits_url":"https://api.github.com/repos/mozilla/balrog/pulls/218/commits","review_comments_url":"https://api.github.com/repos/mozilla/balrog/pulls/218/comments","review_comment_url":"https://api.github.com/repos/mozilla/balrog/pulls/comments{/number}","comments_url":"https://api.github.com/repos/mozilla/balrog/issues/218/comments","statuses_url":"https://api.github.com/repos/mozilla/balrog/statuses/4f45c986daedcec54fa341a689d53ec3bf09e76b","head":{"label":"mozbhearsum:rs-apis","ref":"rs-apis","sha":"4f45c986daedcec54fa341a689d53ec3bf09e76b","user":{"login":"mozbhearsum","id":49649,"avatar_url":"https://avatars.githubusercontent.com/u/49649?v=3","gravatar_id":"","url":"https://api.github.com/users/mozbhearsum","html_url":"https://github.com/mozbhearsum","followers_url":"https://api.github.com/users/mozbhearsum/followers","following_url":"https://api.github.com/users/mozbhearsum/following{/other_user}","gists_url":"https://api.github.com/users/mozbhearsum/gists{/gist_id}","starred_url":"https://api.github.com/users/mozbhearsum/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozbhearsum/subscriptions","organizations_url":"https://api.github.com/users/mozbhearsum/orgs","repos_url":"https://api.github.com/users/mozbhearsum/repos","events_url":"https://api.github.com/users/mozbhearsum/events{/privacy}","received_events_url":"https://api.github.com/users/mozbhearsum/received_events","type":"User","site_admin":false},"repo":{"id":2570483,"name":"balrog","full_name":"mozbhearsum/balrog","owner":{"login":"mozbhearsum","id":49649,"avatar_url":"https://avatars.githubusercontent.com/u/49649?v=3","gravatar_id":"","url":"https://api.github.com/users/mozbhearsum","html_url":"https://github.com/mozbhearsum","followers_url":"https://api.github.com/users/mozbhearsum/followers","following_url":"https://api.github.com/users/mozbhearsum/following{/other_user}","gists_url":"https://api.github.com/users/mozbhearsum/gists{/gist_id}","starred_url":"https://api.github.com/users/mozbhearsum/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozbhearsum/subscriptions","organizations_url":"https://api.github.com/users/mozbhearsum/orgs","repos_url":"https://api.github.com/users/mozbhearsum/repos","events_url":"https://api.github.com/users/mozbhearsum/events{/privacy}","received_events_url":"https://api.github.com/users/mozbhearsum/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/mozbhearsum/balrog","description":"Next generation update service for Mozilla products","fork":true,"url":"https://api.github.com/repos/mozbhearsum/balrog","forks_url":"https://api.github.com/repos/mozbhearsum/balrog/forks","keys_url":"https://api.github.com/repos/mozbhearsum/balrog/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mozbhearsum/balrog/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mozbhearsum/balrog/teams","hooks_url":"https://api.github.com/repos/mozbhearsum/balrog/hooks","issue_events_url":"https://api.github.com/repos/mozbhearsum/balrog/issues/events{/number}","events_url":"https://api.github.com/repos/mozbhearsum/balrog/events","assignees_url":"https://api.github.com/repos/mozbhearsum/balrog/assignees{/user}","branches_url":"https://api.github.com/repos/mozbhearsum/balrog/branches{/branch}","tags_url":"https://api.github.com/repos/mozbhearsum/balrog/tags","blobs_url":"https://api.github.com/repos/mozbhearsum/balrog/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mozbhearsum/balrog/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mozbhearsum/balrog/git/refs{/sha}","trees_url":"https://api.github.com/repos/mozbhearsum/balrog/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mozbhearsum/balrog/statuses/{sha}","languages_url":"https://api.github.com/repos/mozbhearsum/balrog/languages","stargazers_url":"https://api.github.com/repos/mozbhearsum/balrog/stargazers","contributors_url":"https://api.github.com/repos/mozbhearsum/balrog/contributors","subscribers_url":"https://api.github.com/repos/mozbhearsum/balrog/subscribers","subscription_url":"https://api.github.com/repos/mozbhearsum/balrog/subscription","commits_url":"https://api.github.com/repos/mozbhearsum/balrog/commits{/sha}","git_commits_url":"https://api.github.com/repos/mozbhearsum/balrog/git/commits{/sha}","comments_url":"https://api.github.com/repos/mozbhearsum/balrog/comments{/number}","issue_comment_url":"https://api.github.com/repos/mozbhearsum/balrog/issues/comments{/number}","contents_url":"https://api.github.com/repos/mozbhearsum/balrog/contents/{+path}","compare_url":"https://api.github.com/repos/mozbhearsum/balrog/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mozbhearsum/balrog/merges","archive_url":"https://api.github.com/repos/mozbhearsum/balrog/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mozbhearsum/balrog/downloads","issues_url":"https://api.github.com/repos/mozbhearsum/balrog/issues{/number}","pulls_url":"https://api.github.com/repos/mozbhearsum/balrog/pulls{/number}","milestones_url":"https://api.github.com/repos/mozbhearsum/balrog/milestones{/number}","notifications_url":"https://api.github.com/repos/mozbhearsum/balrog/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mozbhearsum/balrog/labels{/name}","releases_url":"https://api.github.com/repos/mozbhearsum/balrog/releases{/id}","deployments_url":"https://api.github.com/repos/mozbhearsum/balrog/deployments","created_at":"2011-10-13T15:58:54Z","updated_at":"2016-08-31T17:23:24Z","pushed_at":"2017-02-13T14:09:22Z","git_url":"git://github.com/mozbhearsum/balrog.git","ssh_url":"git@github.com:mozbhearsum/balrog.git","clone_url":"https://github.com/mozbhearsum/balrog.git","svn_url":"https://github.com/mozbhearsum/balrog","homepage":"","size":64596,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":0,"forks":1,"open_issues":0,"watchers":2,"default_branch":"master"}},"base":{"label":"mozilla:master","ref":"master","sha":"0ec835ab88464581e3df1a707c6baec85365e3b0","user":{"login":"mozilla","id":131524,"avatar_url":"https://avatars.githubusercontent.com/u/131524?v=3","gravatar_id":"","url":"https://api.github.com/users/mozilla","html_url":"https://github.com/mozilla","followers_url":"https://api.github.com/users/mozilla/followers","following_url":"https://api.github.com/users/mozilla/following{/other_user}","gists_url":"https://api.github.com/users/mozilla/gists{/gist_id}","starred_url":"https://api.github.com/users/mozilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozilla/subscriptions","organizations_url":"https://api.github.com/users/mozilla/orgs","repos_url":"https://api.github.com/users/mozilla/repos","events_url":"https://api.github.com/users/mozilla/events{/privacy}","received_events_url":"https://api.github.com/users/mozilla/received_events","type":"Organization","site_admin":false},"repo":{"id":2570444,"name":"balrog","full_name":"mozilla/balrog","owner":{"login":"mozilla","id":131524,"avatar_url":"https://avatars.githubusercontent.com/u/131524?v=3","gravatar_id":"","url":"https://api.github.com/users/mozilla","html_url":"https://github.com/mozilla","followers_url":"https://api.github.com/users/mozilla/followers","following_url":"https://api.github.com/users/mozilla/following{/other_user}","gists_url":"https://api.github.com/users/mozilla/gists{/gist_id}","starred_url":"https://api.github.com/users/mozilla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mozilla/subscriptions","organizations_url":"https://api.github.com/users/mozilla/orgs","repos_url":"https://api.github.com/users/mozilla/repos","events_url":"https://api.github.com/users/mozilla/events{/privacy}","received_events_url":"https://api.github.com/users/mozilla/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mozilla/balrog","description":"Next generation update service for Mozilla products","fork":false,"url":"https://api.github.com/repos/mozilla/balrog","forks_url":"https://api.github.com/repos/mozilla/balrog/forks","keys_url":"https://api.github.com/repos/mozilla/balrog/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mozilla/balrog/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mozilla/balrog/teams","hooks_url":"https://api.github.com/repos/mozilla/balrog/hooks","issue_events_url":"https://api.github.com/repos/mozilla/balrog/issues/events{/number}","events_url":"https://api.github.com/repos/mozilla/balrog/events","assignees_url":"https://api.github.com/repos/mozilla/balrog/assignees{/user}","branches_url":"https://api.github.com/repos/mozilla/balrog/branches{/branch}","tags_url":"https://api.github.com/repos/mozilla/balrog/tags","blobs_url":"https://api.github.com/repos/mozilla/balrog/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mozilla/balrog/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mozilla/balrog/git/refs{/sha}","trees_url":"https://api.github.com/repos/mozilla/balrog/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mozilla/balrog/statuses/{sha}","languages_url":"https://api.github.com/repos/mozilla/balrog/languages","stargazers_url":"https://api.github.com/repos/mozilla/balrog/stargazers","contributors_url":"https://api.github.com/repos/mozilla/balrog/contributors","subscribers_url":"https://api.github.com/repos/mozilla/balrog/subscribers","subscription_url":"https://api.github.com/repos/mozilla/balrog/subscription","commits_url":"https://api.github.com/repos/mozilla/balrog/commits{/sha}","git_commits_url":"https://api.github.com/repos/mozilla/balrog/git/commits{/sha}","comments_url":"https://api.github.com/repos/mozilla/balrog/comments{/number}","issue_comment_url":"https://api.github.com/repos/mozilla/balrog/issues/comments{/number}","contents_url":"https://api.github.com/repos/mozilla/balrog/contents/{+path}","compare_url":"https://api.github.com/repos/mozilla/balrog/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mozilla/balrog/merges","archive_url":"https://api.github.com/repos/mozilla/balrog/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mozilla/balrog/downloads","issues_url":"https://api.github.com/repos/mozilla/balrog/issues{/number}","pulls_url":"https://api.github.com/repos/mozilla/balrog/pulls{/number}","milestones_url":"https://api.github.com/repos/mozilla/balrog/milestones{/number}","notifications_url":"https://api.github.com/repos/mozilla/balrog/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mozilla/balrog/labels{/name}","releases_url":"https://api.github.com/repos/mozilla/balrog/releases{/id}","deployments_url":"https://api.github.com/repos/mozilla/balrog/deployments","created_at":"2011-10-13T15:51:47Z","updated_at":"2017-02-12T16:47:56Z","pushed_at":"2017-02-13T15:02:05Z","git_url":"git://github.com/mozilla/balrog.git","ssh_url":"git@github.com:mozilla/balrog.git","clone_url":"https://github.com/mozilla/balrog.git","svn_url":"https://github.com/mozilla/balrog","homepage":"","size":61541,"stargazers_count":36,"watchers_count":36,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":68,"mirror_url":null,"open_issues_count":4,"forks":68,"open_issues":4,"watchers":36,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/mozilla/balrog/pulls/218"},"html":{"href":"https://github.com/mozilla/balrog/pull/218"},"issue":{"href":"https://api.github.com/repos/mozilla/balrog/issues/218"},"comments":{"href":"https://api.github.com/repos/mozilla/balrog/issues/218/comments"},"review_comments":{"href":"https://api.github.com/repos/mozilla/balrog/pulls/218/comments"},"review_comment":{"href":"https://api.github.com/repos/mozilla/balrog/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/mozilla/balrog/pulls/218/commits"},"statuses":{"href":"https://api.github.com/repos/mozilla/balrog/statuses/4f45c986daedcec54fa341a689d53ec3bf09e76b"}}}}
{"actor":{"display_login":"mozbhearsum"}}