{"action":"created","comment":{"url":"https://api.github.com/repos/Nextdoor/kingpin/pulls/comments/35879960","id":35879960,"diff_hunk":"@@ -0,0 +1,400 @@\n+# Licensed under the Apache License, Version 2.0 (the \"License\");\n+# you may not use this file except in compliance with the License.\n+# You may obtain a copy of the License at\n+#\n+# http://www.apache.org/licenses/LICENSE-2.0\n+#\n+# Unless required by applicable law or agreed to in writing, software\n+# distributed under the License is distributed on an \"AS IS\" BASIS,\n+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+# See the License for the specific language governing permissions and\n+# limitations under the License.\n+#\n+# Copyright 2014 Nextdoor.com, Inc\n+\n+\"\"\"\n+:mod:`kingpin.actors.packagecloud`\n+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n+\n+The packagecloud actor allows you to perform maintenance operations on\n+repositories hosted by packagecloud.io using their API:\n+\n+https://packagecloud.io/docs/api\n+\n+**Required Environment Variables**\n+\n+:PACKAGECLOUD_ACCOUNT:\n+  packagecloud account name, i.e. https://packagecloud.io/PACKAGECLOUD_ACCOUNT\n+\n+:PACKAGECLOUD_TOKEN:\n+  packagecloud API Token\n+\"\"\"\n+\n+import datetime\n+import logging\n+import os\n+import re\n+import sys\n+\n+from tornado import gen\n+from tornado_rest_client import api\n+\n+from kingpin.actors import base\n+from kingpin.actors import exceptions\n+from kingpin.constants import REQUIRED\n+\n+log = logging.getLogger(__name__)\n+\n+__author__ = 'Charles McLaughlin <charles@nextdoor.com>'\n+\n+ACCOUNT = os.getenv('PACKAGECLOUD_ACCOUNT', None)\n+TOKEN = os.getenv('PACKAGECLOUD_TOKEN', None)\n+\n+\n+class PackagecloudAPI(api.RestConsumer):\n+\n+    ENDPOINT = 'https://packagecloud.io/api/v1/'\n+    CONFIG = {\n+        'attrs': {\n+            'packages': {\n+                'path': ('repos/%account%/%repo%/packages.json'\n+                         '?per_page={}'.format(sys.maxint)),\n+                'http_methods': {'get': {}}\n+            },\n+            'delete': {\n+                'path': 'repos/%account%/%repo%/%distro_version%/%filename%',\n+                'http_methods': {'delete': {}}\n+            },\n+        },\n+        'auth': {\n+            'user': TOKEN,\n+            'pass': ''\n+        }\n+    }\n+\n+\n+class PackagecloudBase(base.HTTPBaseActor):\n+\n+    \"\"\"Simple packagecloud Abstract Base Object\"\"\"\n+\n+    def __init__(self, *args, **kwargs):\n+        \"\"\"Check required environment variables.\"\"\"\n+        super(PackagecloudBase, self).__init__(*args, **kwargs)\n+\n+        if not ACCOUNT:\n+            raise exceptions.InvalidCredentials(\n+                'Missing the \"PACKAGECLOUD_ACCOUNT\" environment variable.')\n+\n+        if not TOKEN:\n+            raise exceptions.InvalidCredentials(\n+                'Missing the \"PACKAGECLOUD_TOKEN\" environment variable.')\n+\n+        rest_client = api.RestClient()\n+        self._packagecloud_client = PackagecloudAPI(client=rest_client)\n+\n+    @gen.coroutine\n+    def _get_all_packages(self, repo):\n+        \"\"\"Simple method for fetching a dictionary of all packages in a repo\n+\n+        Args:\n+            repo: name of the packagecloud repo to fetch from\n+\n+        Returns:\n+            A hash of the packages.\n+        \"\"\"\n+        all_packages = yield self._packagecloud_client.packages(\n+            token=TOKEN, account=ACCOUNT, repo=repo).http_get()\n+        raise gen.Return(all_packages)\n+\n+    @gen.coroutine\n+    def _get_package_versions(self, name, all_packages):\n+        \"\"\"Find all versions of a given package.\n+\n+        Args:\n+            name: name of the package to look for\n+            all_packages: hash of all the packages, as returned by the API\n+\n+        Returns:\n+            A hash of package versions sorted by creation date\n+        \"\"\"\n+        versions = [{\n+            'created_at': datetime.datetime.strptime(\n+                package['created_at'], '%Y-%m-%dT%H:%M:%S.%fZ'),\n+            'distro_version': package['distro_version'],\n+            'filename': package['package_html_url'].split('/')[-1],\n+            'name': package['name']\n+        } for package in all_packages if package['name'] == name]\n+\n+        versions.sort(key=lambda x: x.get('created_at'), reverse=True)\n+        raise gen.Return(versions)\n+\n+    @gen.coroutine\n+    def _get_packages_list_to_delete(self, packages_to_delete, all_packages):\n+        \"\"\"Extracts a list of unique package names to delete\n+\n+        Args:\n+            packages_to_delete: regex of package names to delete\n+            all_packages: hash of all the packages, as returned by the API\n+\n+        Returns:\n+            A list of unique package names that match the delete pattern.\n+            \"\"\"\n+        pattern = re.compile(packages_to_delete)\n+        packages_list_to_delete = [package['name'] for package in all_packages\n+                                   if pattern.match(package['name'])]\n+        packages_list_to_delete = set(packages_list_to_delete)\n+        raise gen.Return(packages_list_to_delete)\n+\n+    @gen.coroutine\n+    def _delete(self, packages_to_delete, repo, older_than=0,\n+                number_to_keep=0):\n+        \"\"\"Generic packagecloud delete method, optionally supporting deleting\n+        old packages by date and/or keeping a certain number of packages.\n+\n+        Args:\n+            packages_to_delete: Regex of packages to delete, e.g. pkg1|pkg2\n+            repo: name of the packagecloud repo to delete from\n+            older_than: Delete packages created before this number of seconds\n+            number_to_keep: Keep at least this number of each package\n+\n+        Returns:\n+            A list of the packages that were deleted\n+        \"\"\"\n+        all_packages = yield self._get_all_packages(repo=repo)\n+        packages_list_to_delete = yield self._get_packages_list_to_delete(\n+            packages_to_delete, all_packages)\n+        deleted_packages = []\n+\n+        # Loop through each unique package to delete\n+        for name in packages_list_to_delete:\n+            package_versions = yield self._get_package_versions(\n+                name, all_packages)\n+\n+            # Delete individual packages if they meet our criteria\n+            number_in_repo = len(package_versions)\n+            for package in package_versions:\n+                if number_in_repo > number_to_keep:\n+                    if older_than:\n+                        if datetime.datetime.now() - package['created_at'] > \\\n+                           datetime.timedelta(seconds=older_than):\n+                            pass\n+                        else:\n+                            continue\n+\n+                    msg = '%s/%s/%s' % (\n+                        repo, package['distro_version'], package['filename'])\n+                    if self._dry:\n+                        log.info('Would have deleted %s' % msg)\n+                    else:\n+                        log.info('Deleting %s' % msg)\n+\n+                        yield self._packagecloud_client.delete(\n+                            token=TOKEN, account=ACCOUNT, repo=repo,\n+                            distro_version=package['distro_version'],\n+                            filename=package['filename']\n+                        ).http_delete()\n+\n+                    # Decrement list of packages to track how many are left\n+                    number_in_repo = number_in_repo - 1\n+                    # Keeping track of removed packages for return value\n+                    deleted_packages.append(package)\n+\n+        raise gen.Return(deleted_packages)\n+\n+\n+class Delete(PackagecloudBase):\n+\n+    \"\"\"packagecloud Delete Actor Class\n+\n+    **Options**\n+\n+    :number_to_keep:\n+      Keep at least this number of each package\n+      (defaults to *0*)\n+\n+    :packages_to_delete:\n+      Regex of packages to delete, e.g. pkg1|pkg2\n+\n+    :repo:\n+      Which packagecloud repo to delete from\n+\n+    **Examples**\n+\n+    .. code-block:: json\n+\n+    { \"desc\": \"packagecloud Delete example\",\n+      \"actor\": \"packagecloud.Delete\",\n+      \"options\": {\n+          \"number_to_keep\": 10,\n+          \"packages_to_delete\": \"deleteme\",\n+          \"repo\": \"test\"\n+      }\n+    }\n+\n+    \"\"\"\n+\n+    all_options = {\n+        'number_to_keep': (\n+            int, 0,\n+            'Keep at least this number of each package'),\n+        'packages_to_delete': (\n+            str, REQUIRED,\n+            'Regex of packages to delete, e.g. pkg1|pkg2'),\n+        'repo': (\n+            str, REQUIRED,\n+            'Which packagecloud repo to delete from'),\n+    }\n+\n+    @gen.coroutine\n+    def _execute(self):\n+        \"\"\"Deletes all packages that match the `packages_to_delete` pattern\"\"\"\n+        yield self._delete(\n+            packages_to_delete=self.option('packages_to_delete'),\n+            number_to_keep=self.option('number_to_keep'),\n+            repo=self.option('repo'))\n+\n+\n+class DeleteByDate(PackagecloudBase):\n+\n+    \"\"\"packagecloud DeleteByDate Actor Class\n+\n+    **Options**\n+\n+    :number_to_keep:\n+      Keep at least this number of each package\n+      (defaults to *0*)\n+\n+    :older_than:\n+      Delete packages created before this number of seconds\n+\n+    :packages_to_delete:\n+      Regex of packages to delete, e.g. pkg1|pkg2\n+\n+    :repo:\n+      Which packagecloud repo to delete from\n+\n+    **Examples**\n+\n+    .. code-block:: json\n+\n+    { \"desc\": \"packagecloud DeleteByDate example\",\n+      \"actor\": \"packagecloud.DeleteByDate\",\n+      \"options\": {\n+          \"number_to_keep\": 10,\n+          \"older_than\": 600,\n+          \"packages_to_delete\": \"deleteme\",\n+          \"repo\": \"test\"\n+      }\n+    }\n+\n+    \"\"\"\n+\n+    all_options = {\n+        'number_to_keep': (\n+            int, 0,\n+            'Keep at least this number of each package'),\n+        'older_than': (\n+            int, REQUIRED,\n+            'Delete packages created before this number of seconds'),\n+        'packages_to_delete': (\n+            str, REQUIRED,\n+            'Regex of packages to delete, e.g. pkg1|pkg2'),\n+        'repo': (str, REQUIRED,\n+                 'Which packagecloud repo to delete from')\n+    }\n+\n+    @gen.coroutine\n+    def _execute(self):\n+        \"\"\"Deletes all packages that match the `packages_to_delete` pattern and\n+        where created before `older_than` number of seconds\"\"\"\n+        yield self._delete(\n+            packages_to_delete=self.option('packages_to_delete'),\n+            number_to_keep=self.option('number_to_keep'),\n+            older_than=self.option('older_than'),\n+            repo=self.option('repo'))\n+\n+\n+class WaitForPackage(PackagecloudBase):\n+\n+    \"\"\"packagecloud WaitForPackage Actor","path":"kingpin/actors/packagecloud.py","position":319,"original_position":319,"commit_id":"4d3b84dccc5b196cf83c451892d73c006ca38042","original_commit_id":"4d3b84dccc5b196cf83c451892d73c006ca38042","user":{"login":"diranged","id":768067,"avatar_url":"https://avatars.githubusercontent.com/u/768067?v=3","gravatar_id":"","url":"https://api.github.com/users/diranged","html_url":"https://github.com/diranged","followers_url":"https://api.github.com/users/diranged/followers","following_url":"https://api.github.com/users/diranged/following{/other_user}","gists_url":"https://api.github.com/users/diranged/gists{/gist_id}","starred_url":"https://api.github.com/users/diranged/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/diranged/subscriptions","organizations_url":"https://api.github.com/users/diranged/orgs","repos_url":"https://api.github.com/users/diranged/repos","events_url":"https://api.github.com/users/diranged/events{/privacy}","received_events_url":"https://api.github.com/users/diranged/received_events","type":"User","site_admin":false},"body":"Same comment as line 207","created_at":"2015-07-30T15:01:28Z","updated_at":"2015-07-30T15:01:28Z","html_url":"https://github.com/Nextdoor/kingpin/pull/277#discussion_r35879960","pull_request_url":"https://api.github.com/repos/Nextdoor/kingpin/pulls/277","_links":{"self":{"href":"https://api.github.com/repos/Nextdoor/kingpin/pulls/comments/35879960"},"html":{"href":"https://github.com/Nextdoor/kingpin/pull/277#discussion_r35879960"},"pull_request":{"href":"https://api.github.com/repos/Nextdoor/kingpin/pulls/277"}}},"pull_request":{"url":"https://api.github.com/repos/Nextdoor/kingpin/pulls/277","id":39137189,"html_url":"https://github.com/Nextdoor/kingpin/pull/277","diff_url":"https://github.com/Nextdoor/kingpin/pull/277.diff","patch_url":"https://github.com/Nextdoor/kingpin/pull/277.patch","issue_url":"https://api.github.com/repos/Nextdoor/kingpin/issues/277","number":277,"state":"open","locked":false,"title":"Packagecloud Actor","user":{"login":"cmclaughlin","id":1061109,"avatar_url":"https://avatars.githubusercontent.com/u/1061109?v=3","gravatar_id":"","url":"https://api.github.com/users/cmclaughlin","html_url":"https://github.com/cmclaughlin","followers_url":"https://api.github.com/users/cmclaughlin/followers","following_url":"https://api.github.com/users/cmclaughlin/following{/other_user}","gists_url":"https://api.github.com/users/cmclaughlin/gists{/gist_id}","starred_url":"https://api.github.com/users/cmclaughlin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cmclaughlin/subscriptions","organizations_url":"https://api.github.com/users/cmclaughlin/orgs","repos_url":"https://api.github.com/users/cmclaughlin/repos","events_url":"https://api.github.com/users/cmclaughlin/events{/privacy}","received_events_url":"https://api.github.com/users/cmclaughlin/received_events","type":"User","site_admin":false},"body":"@siminm @diranged  Review please.  No tests so far, so just let me know if this looks good so far and if so, I'll add tests. ","created_at":"2015-07-03T00:13:58Z","updated_at":"2015-07-30T15:01:28Z","closed_at":null,"merged_at":null,"merge_commit_sha":"adaf8a67a64efbe8f3af0a61442b7c800a2b9db7","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/Nextdoor/kingpin/pulls/277/commits","review_comments_url":"https://api.github.com/repos/Nextdoor/kingpin/pulls/277/comments","review_comment_url":"https://api.github.com/repos/Nextdoor/kingpin/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Nextdoor/kingpin/issues/277/comments","statuses_url":"https://api.github.com/repos/Nextdoor/kingpin/statuses/4d3b84dccc5b196cf83c451892d73c006ca38042","head":{"label":"Nextdoor:packagecloud","ref":"packagecloud","sha":"4d3b84dccc5b196cf83c451892d73c006ca38042","user":{"login":"Nextdoor","id":605176,"avatar_url":"https://avatars.githubusercontent.com/u/605176?v=3","gravatar_id":"","url":"https://api.github.com/users/Nextdoor","html_url":"https://github.com/Nextdoor","followers_url":"https://api.github.com/users/Nextdoor/followers","following_url":"https://api.github.com/users/Nextdoor/following{/other_user}","gists_url":"https://api.github.com/users/Nextdoor/gists{/gist_id}","starred_url":"https://api.github.com/users/Nextdoor/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Nextdoor/subscriptions","organizations_url":"https://api.github.com/users/Nextdoor/orgs","repos_url":"https://api.github.com/users/Nextdoor/repos","events_url":"https://api.github.com/users/Nextdoor/events{/privacy}","received_events_url":"https://api.github.com/users/Nextdoor/received_events","type":"Organization","site_admin":false},"repo":{"id":22902460,"name":"kingpin","full_name":"Nextdoor/kingpin","owner":{"login":"Nextdoor","id":605176,"avatar_url":"https://avatars.githubusercontent.com/u/605176?v=3","gravatar_id":"","url":"https://api.github.com/users/Nextdoor","html_url":"https://github.com/Nextdoor","followers_url":"https://api.github.com/users/Nextdoor/followers","following_url":"https://api.github.com/users/Nextdoor/following{/other_user}","gists_url":"https://api.github.com/users/Nextdoor/gists{/gist_id}","starred_url":"https://api.github.com/users/Nextdoor/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Nextdoor/subscriptions","organizations_url":"https://api.github.com/users/Nextdoor/orgs","repos_url":"https://api.github.com/users/Nextdoor/repos","events_url":"https://api.github.com/users/Nextdoor/events{/privacy}","received_events_url":"https://api.github.com/users/Nextdoor/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Nextdoor/kingpin","description":"Deployment Automation Engine","fork":false,"url":"https://api.github.com/repos/Nextdoor/kingpin","forks_url":"https://api.github.com/repos/Nextdoor/kingpin/forks","keys_url":"https://api.github.com/repos/Nextdoor/kingpin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Nextdoor/kingpin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Nextdoor/kingpin/teams","hooks_url":"https://api.github.com/repos/Nextdoor/kingpin/hooks","issue_events_url":"https://api.github.com/repos/Nextdoor/kingpin/issues/events{/number}","events_url":"https://api.github.com/repos/Nextdoor/kingpin/events","assignees_url":"https://api.github.com/repos/Nextdoor/kingpin/assignees{/user}","branches_url":"https://api.github.com/repos/Nextdoor/kingpin/branches{/branch}","tags_url":"https://api.github.com/repos/Nextdoor/kingpin/tags","blobs_url":"https://api.github.com/repos/Nextdoor/kingpin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Nextdoor/kingpin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Nextdoor/kingpin/git/refs{/sha}","trees_url":"https://api.github.com/repos/Nextdoor/kingpin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Nextdoor/kingpin/statuses/{sha}","languages_url":"https://api.github.com/repos/Nextdoor/kingpin/languages","stargazers_url":"https://api.github.com/repos/Nextdoor/kingpin/stargazers","contributors_url":"https://api.github.com/repos/Nextdoor/kingpin/contributors","subscribers_url":"https://api.github.com/repos/Nextdoor/kingpin/subscribers","subscription_url":"https://api.github.com/repos/Nextdoor/kingpin/subscription","commits_url":"https://api.github.com/repos/Nextdoor/kingpin/commits{/sha}","git_commits_url":"https://api.github.com/repos/Nextdoor/kingpin/git/commits{/sha}","comments_url":"https://api.github.com/repos/Nextdoor/kingpin/comments{/number}","issue_comment_url":"https://api.github.com/repos/Nextdoor/kingpin/issues/comments{/number}","contents_url":"https://api.github.com/repos/Nextdoor/kingpin/contents/{+path}","compare_url":"https://api.github.com/repos/Nextdoor/kingpin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Nextdoor/kingpin/merges","archive_url":"https://api.github.com/repos/Nextdoor/kingpin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Nextdoor/kingpin/downloads","issues_url":"https://api.github.com/repos/Nextdoor/kingpin/issues{/number}","pulls_url":"https://api.github.com/repos/Nextdoor/kingpin/pulls{/number}","milestones_url":"https://api.github.com/repos/Nextdoor/kingpin/milestones{/number}","notifications_url":"https://api.github.com/repos/Nextdoor/kingpin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Nextdoor/kingpin/labels{/name}","releases_url":"https://api.github.com/repos/Nextdoor/kingpin/releases{/id}","created_at":"2014-08-13T04:23:40Z","updated_at":"2015-07-16T18:22:18Z","pushed_at":"2015-07-28T23:25:04Z","git_url":"git://github.com/Nextdoor/kingpin.git","ssh_url":"git@github.com:Nextdoor/kingpin.git","clone_url":"https://github.com/Nextdoor/kingpin.git","svn_url":"https://github.com/Nextdoor/kingpin","homepage":"","size":3183,"stargazers_count":3,"watchers_count":3,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2,"mirror_url":null,"open_issues_count":17,"forks":2,"open_issues":17,"watchers":3,"default_branch":"master"}},"base":{"label":"Nextdoor:master","ref":"master","sha":"e51d6901c4a4286dfa561c36002d145458e6f602","user":{"login":"Nextdoor","id":605176,"avatar_url":"https://avatars.githubusercontent.com/u/605176?v=3","gravatar_id":"","url":"https://api.github.com/users/Nextdoor","html_url":"https://github.com/Nextdoor","followers_url":"https://api.github.com/users/Nextdoor/followers","following_url":"https://api.github.com/users/Nextdoor/following{/other_user}","gists_url":"https://api.github.com/users/Nextdoor/gists{/gist_id}","starred_url":"https://api.github.com/users/Nextdoor/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Nextdoor/subscriptions","organizations_url":"https://api.github.com/users/Nextdoor/orgs","repos_url":"https://api.github.com/users/Nextdoor/repos","events_url":"https://api.github.com/users/Nextdoor/events{/privacy}","received_events_url":"https://api.github.com/users/Nextdoor/received_events","type":"Organization","site_admin":false},"repo":{"id":22902460,"name":"kingpin","full_name":"Nextdoor/kingpin","owner":{"login":"Nextdoor","id":605176,"avatar_url":"https://avatars.githubusercontent.com/u/605176?v=3","gravatar_id":"","url":"https://api.github.com/users/Nextdoor","html_url":"https://github.com/Nextdoor","followers_url":"https://api.github.com/users/Nextdoor/followers","following_url":"https://api.github.com/users/Nextdoor/following{/other_user}","gists_url":"https://api.github.com/users/Nextdoor/gists{/gist_id}","starred_url":"https://api.github.com/users/Nextdoor/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Nextdoor/subscriptions","organizations_url":"https://api.github.com/users/Nextdoor/orgs","repos_url":"https://api.github.com/users/Nextdoor/repos","events_url":"https://api.github.com/users/Nextdoor/events{/privacy}","received_events_url":"https://api.github.com/users/Nextdoor/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Nextdoor/kingpin","description":"Deployment Automation Engine","fork":false,"url":"https://api.github.com/repos/Nextdoor/kingpin","forks_url":"https://api.github.com/repos/Nextdoor/kingpin/forks","keys_url":"https://api.github.com/repos/Nextdoor/kingpin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Nextdoor/kingpin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Nextdoor/kingpin/teams","hooks_url":"https://api.github.com/repos/Nextdoor/kingpin/hooks","issue_events_url":"https://api.github.com/repos/Nextdoor/kingpin/issues/events{/number}","events_url":"https://api.github.com/repos/Nextdoor/kingpin/events","assignees_url":"https://api.github.com/repos/Nextdoor/kingpin/assignees{/user}","branches_url":"https://api.github.com/repos/Nextdoor/kingpin/branches{/branch}","tags_url":"https://api.github.com/repos/Nextdoor/kingpin/tags","blobs_url":"https://api.github.com/repos/Nextdoor/kingpin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Nextdoor/kingpin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Nextdoor/kingpin/git/refs{/sha}","trees_url":"https://api.github.com/repos/Nextdoor/kingpin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Nextdoor/kingpin/statuses/{sha}","languages_url":"https://api.github.com/repos/Nextdoor/kingpin/languages","stargazers_url":"https://api.github.com/repos/Nextdoor/kingpin/stargazers","contributors_url":"https://api.github.com/repos/Nextdoor/kingpin/contributors","subscribers_url":"https://api.github.com/repos/Nextdoor/kingpin/subscribers","subscription_url":"https://api.github.com/repos/Nextdoor/kingpin/subscription","commits_url":"https://api.github.com/repos/Nextdoor/kingpin/commits{/sha}","git_commits_url":"https://api.github.com/repos/Nextdoor/kingpin/git/commits{/sha}","comments_url":"https://api.github.com/repos/Nextdoor/kingpin/comments{/number}","issue_comment_url":"https://api.github.com/repos/Nextdoor/kingpin/issues/comments{/number}","contents_url":"https://api.github.com/repos/Nextdoor/kingpin/contents/{+path}","compare_url":"https://api.github.com/repos/Nextdoor/kingpin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Nextdoor/kingpin/merges","archive_url":"https://api.github.com/repos/Nextdoor/kingpin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Nextdoor/kingpin/downloads","issues_url":"https://api.github.com/repos/Nextdoor/kingpin/issues{/number}","pulls_url":"https://api.github.com/repos/Nextdoor/kingpin/pulls{/number}","milestones_url":"https://api.github.com/repos/Nextdoor/kingpin/milestones{/number}","notifications_url":"https://api.github.com/repos/Nextdoor/kingpin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Nextdoor/kingpin/labels{/name}","releases_url":"https://api.github.com/repos/Nextdoor/kingpin/releases{/id}","created_at":"2014-08-13T04:23:40Z","updated_at":"2015-07-16T18:22:18Z","pushed_at":"2015-07-28T23:25:04Z","git_url":"git://github.com/Nextdoor/kingpin.git","ssh_url":"git@github.com:Nextdoor/kingpin.git","clone_url":"https://github.com/Nextdoor/kingpin.git","svn_url":"https://github.com/Nextdoor/kingpin","homepage":"","size":3183,"stargazers_count":3,"watchers_count":3,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2,"mirror_url":null,"open_issues_count":17,"forks":2,"open_issues":17,"watchers":3,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/Nextdoor/kingpin/pulls/277"},"html":{"href":"https://github.com/Nextdoor/kingpin/pull/277"},"issue":{"href":"https://api.github.com/repos/Nextdoor/kingpin/issues/277"},"comments":{"href":"https://api.github.com/repos/Nextdoor/kingpin/issues/277/comments"},"review_comments":{"href":"https://api.github.com/repos/Nextdoor/kingpin/pulls/277/comments"},"review_comment":{"href":"https://api.github.com/repos/Nextdoor/kingpin/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Nextdoor/kingpin/pulls/277/commits"},"statuses":{"href":"https://api.github.com/repos/Nextdoor/kingpin/statuses/4d3b84dccc5b196cf83c451892d73c006ca38042"}}}}