{"action":"created","comment":{"url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/comments/72788058","id":72788058,"diff_hunk":"@@ -0,0 +1,433 @@\n+# !/usr/bin/env python\n+# -*- coding: utf-8 -*-\n+\n+###############################################################################\n+#  Copyright Kitware Inc.\n+#\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+\n+\n+import six\n+import json\n+from six import iteritems\n+import hashlib\n+\n+from girder.api.v1.resource import Resource, RestException\n+from .constants import PluginSettings\n+from girder.utility.model_importer import ModelImporter\n+from girder.api.rest import getCurrentUser\n+from girder.api import access\n+from girder.api.describe import Description, describeRoute\n+\n+\n+class DockerResource(Resource):\n+    \"\"\"\n+    Manages the exposed rest api. When the settings are updated te new list\n+    of docker images is checked, pre-loaded images will be ignored. New images\n+    will cause a job to generate the cli handler and generate the rest endpoint\n+    asynchronously.Deleted images will result in the removal of the rest api\n+    endpoint though docker will still cache the image unless removed manually\n+    (docker rmi image_name)\n+    \"\"\"\n+    resourceName = ''\n+\n+    def __init__(self, name):\n+        super(DockerResource, self).__init__()\n+        self.resourceName = name\n+        DockerResource.resourceName = name\n+        self.route('PUT', (DockerResource.resourceName, 'docker_image'),\n+                   self.appendImage)\n+        self.route('DELETE', (DockerResource.resourceName, 'docker_image'),\n+                   self.deleteImage)\n+        self.route('GET', (DockerResource.resourceName, 'docker_image'),\n+                   self.getDockerImages)\n+\n+    @access.admin\n+    @describeRoute(\n+        Description('list docker images and their clis ')\n+        .notes(\"\"\"Must be a system administrator to call this. If the value\n+                       passed is a valid JSON object.\"\"\")\n+        .errorResponse(\n+            'You are not a system administrator.', 403)\n+        .errorResponse(\n+            'Failed to set system setting.', 500)\n+    )\n+    def getDockerImages(self, params):\n+        data = {}\n+        current = DockerCache(getDockerImageSettings())\n+        for image in current.getDockerImageList():\n+            data[image] = current.getCLIDict(image)\n+        return data\n+\n+    @access.admin\n+    @describeRoute(\n+        Description('Remove a docker image ')\n+        .notes(\n+            \"\"\"Must be a system administrator to call this. If the value\n+                       passed is a valid JSON object.\"\"\")\n+        .param('name', 'The name or a list of names  of the '\n+                       'docker images to be removed', required=True)\n+        .errorResponse('You are not a system administrator.', 403)\n+        .errorResponse('Failed to set system setting.', 500)\n+    )\n+    # TODO delete REST endpoint\n+    def deleteImage(self, params):\n+\n+        self.requireParams(('name',), params)\n+        name = params['name']\n+        name = json.loads(name)\n+\n+        if isinstance(name, list):\n+            for img in name:\n+\n+                if not isinstance(img, six.string_types):\n+                    raise RestException('%s was not a valid string.' % img)\n+            else:\n+                self._deleteImage(name)\n+        elif isinstance(name, six.string_types):\n+\n+            self._deleteImage([name])\n+\n+        else:\n+\n+            raise RestException('name was not a valid JSON list or string.')\n+\n+    def _deleteImage(self, names):\n+        \"\"\"\n+        Removes the docker images and there respective clis from the settings\n+        :param name: The name of the docker image (user/rep:tag)\n+\n+        \"\"\"\n+        currentSettings = DockerCache(getDockerImageSettings())\n+        for name in names:\n+\n+            if not currentSettings.deleteImage(name):\n+                raise RestException('%s does not exist' % name)\n+        # need to remove each cli attribute\n+        # need to remove the clli list attribute\n+        # need to remove the rest route\n+        print \"after delete\", currentSettings.raw()\n+        ModelImporter.model('setting').set(PluginSettings.DOCKER_IMAGES,\n+                                           currentSettings.raw())\n+\n+    @access.admin\n+    @describeRoute(\n+        Description('Add a to the list of images to be loaded ').notes(\n+            \"\"\"Must be a system administrator to call this. If the value\n+                   passed is a valid JSON object, it will be parsed and stored\n+                   as an object.\"\"\").param(\n+            'name', 'The name or a list of names  of the '\n+                    'docker images to be loaded ', required=True)\n+            .errorResponse('You are not a system administrator.', 403)\n+            .errorResponse('Failed to set system setting.', 500)\n+    )\n+    # TODO check the local cache and cloud for different images of same name\n+    # TODO how to handle newer images(take the latest or require a confirmation)\n+    # TODO use image id to confirm equivalence need v2 manifest schema on cloud\n+    # TODO how to handle duplicate clis\n+    # TODO create the new REST endpoints\n+    def appendImage(self, params):\n+        \"\"\"Validates the new images to be added (if they exist or not) and then\n+        attempts to collect xml data to be cached. a job is then called to\n+        update the PluginSettings.DOCKER_IMAGES settings with the new\n+        information\n+        \"\"\"\n+        self.requireParams(('name',), params)\n+        name = params['name']\n+        name = json.loads(name)\n+        self.model('setting').set(PluginSettings.DOCKER_IMAGES, name)\n+\n+    @staticmethod\n+    def appendImageJob(imgs):\n+        \"\"\"\n+        Create an async job to check if each docker image in the list imgs exist\n+        For each existing image cli information will be cached and saved as\n+        a setting\n+        \"\"\"\n+        currentSettings = DockerCache(getDockerImageSettings())\n+\n+        job = ModelImporter.model('job', 'jobs').createLocalJob(\n+            module='girder.plugins.%s.'\n+                   'image_worker' % DockerResource.resourceName,\n+            function='loadXML',\n+            kwargs={'name': imgs,\n+                    'oldSettings': currentSettings.raw()},\n+            title='Updating Settings and Caching xml',\n+            type='%s.images' % DockerResource.resourceName,\n+            user=getCurrentUser(),\n+            public=True,\n+            async=True\n+        )\n+        ModelImporter.model('job', 'jobs').scheduleJob(job)\n+\n+    @staticmethod\n+    def saveSettingsHook(event):\n+        key, val = event.info['key'], event.info['value']\n+\n+        if key == PluginSettings.DOCKER_IMAGES:\n+\n+            if isinstance(val, dict):\n+                # assume receiving data in the format of DockerCache\n+                # validate all data\n+                newSettings = DockerCache(val)\n+                try:\n+                    newSettings.validate()\n+                except DockerImageDataError as err:\n+                    event.defaultPrevented()\n+                    raise RestException(\n+                        'format of new settings was not correct %s', err)\n+            else:\n+                event.defaultPrevented()\n+\n+    @staticmethod\n+    def validateSettings(event):\n+        \"\"\"\n+        Takes three types of inputs, a single image name as a string, alist of\n+        images names or the raw format displayed below. If either an single\n+        docker image name or list of image names is provided cli information on\n+        those images are queried and stored as a setting in the raw format\n+        described below. If the raw format is provided, the structure\n+        and the validty of the contents are checked, if the raw format passes\n+        these check then it replaces the current settings value.\n+        Validity checks include whether\n+        actually images either within the docker cache locally or on an\n+        available registry (default registry being dockerhub)\n+\n+            Data is stored in the following format:\n+                {image_name_hash:\n+                    {\n+                    cli_name:{\n+                        type:\n+                        xml:\n+\n+                        }\n+                    docker_image_name:name\n+                    }\n+                }\n+        \"\"\"\n+\n+        # val should be a dictionary of dictionaries\n+        key, val = event.info['key'], event.info['value']\n+\n+        if key == PluginSettings.DOCKER_IMAGES:\n+            currentSettings = DockerCache(getDockerImageSettings())\n+            if isinstance(val, dict):\n+                # assume receiving data in the format of DockerCache\n+                # validate all data\n+                newSettings = DockerCache(val)\n+                DockerResource.validateDict(newSettings)\n+            elif isinstance(val, list):\n+                # assume this is a list of desired images\n+\n+                for img in val:\n+\n+                    if not isinstance(img, six.string_types):\n+                        raise RestException('%s was not a valid string.' % img)\n+                else:\n+                    # do not want to save the list as the setting\n+\n+                    DockerResource.appendImageJob(val)\n+\n+            elif isinstance(val, six.string_types):\n+                # do not want to save the string as the setting\n+\n+                DockerResource.appendImageJob([val])\n+\n+            else:\n+\n+                raise RestException('name was not a valid JSON list or string.')\n+\n+            event.info['value'] = currentSettings.raw()\n+            event.preventDefault().stopPropagation()\n+\n+    @staticmethod\n+    def validateDict(newSettings):\n+        currentSettings = DockerCache(getDockerImageSettings())\n+        job = ModelImporter.model('job', 'jobs').createLocalJob(\n+            module='girder.plugins.HistomicsTK.image_worker',\n+            function='verifyDictionary',\n+            kwargs={\n+                'newSettings': newSettings.raw(),\n+                'oldSettings': currentSettings.raw()\n+            },\n+\n+            title='Updating Settings ',\n+            type='HistomicsTK.images',\n+            user=getCurrentUser(),\n+            public=True,\n+            async=True\n+        )\n+        ModelImporter.model('job', 'jobs').scheduleJob(job)\n+\n+\n+def getDockerImageSettings():\n+    module_list = ModelImporter.model('setting').get(\n+        PluginSettings.DOCKER_IMAGES)\n+    if module_list is None:\n+        module_list = {}\n+    return module_list\n+\n+\n+class DockerCache():\n+\n+    imageName = 'docker_image_name'\n+    type = 'type'\n+    xml = 'xml'\n+    cli_dict = 'cli_list'\n+\n+    def __init__(self, cache):\n+        \"\"\"\n+        Data is stored in the following format:\n+            {image_name_hash:\n+                {\n+                cli_list:{\n+                    cli_name:{\n+                        type:<type>\n+                        xml:<xml>\n+\n+                            }\n+                        }\n+                docker_image_name:<name>\n+                }\n+            }\n+        \"\"\"\n+        if isinstance(cache, dict):\n+            self.data = cache\n+        elif isinstance(cache, str):\n+            if cache != '':\n+                self.data = json.loads(cache)\n+        else:\n+            self.data = {}\n+\n+    def getDockerImageList(self):\n+\n+        return [str(imgDict[DockerCache.imageName])\n+                for (imgHash, imgDict) in iteritems(self.data)]\n+\n+    def imageAlreadyLoaded(self, name):\n+        imageKey = self._getHashKey(name)\n+        if imageKey in self.data:\n+            return True\n+        else:\n+            return False\n+\n+    def addImage(self, name):\n+\n+        imageKey = self._getHashKey(name)\n+        self.data[imageKey] = {}\n+\n+        self.data[imageKey][DockerCache.imageName] = name\n+        self.data[imageKey][DockerCache.cli_dict] = {}\n+\n+    def addCLI(self, img_name, cli_name, type, xml):\n+\n+        cliData = {}\n+        cliData[DockerCache.type] = type\n+        cliData[DockerCache.xml] = xml\n+\n+        imageKey = self._getHashKey(img_name)\n+        self.data[imageKey][DockerCache.cli_dict][cli_name] = cliData\n+\n+    def raw(self):\n+        return self.data\n+\n+    def deleteImage(self, name):\n+        imageKey = self._getHashKey(name)\n+        if imageKey in self.data:\n+            del self.data[imageKey]\n+            return True\n+        else:\n+            return False\n+\n+    def _getHashKey(self, imgName):\n+        imageKey = hashlib.sha256(imgName.encode()).hexdigest()\n+        return imageKey\n+\n+    def getCLIXML(self, imgName, cli):\n+        imgKey = self._getHashKey(imgName)\n+        if imgKey in self.data:\n+            imageData = self.data[imgKey]\n+\n+        else:\n+            print('no image named %s' % imgName)\n+            return None\n+\n+        if cli in imageData[DockerCache.cli_dict]:\n+\n+            return imageData[DockerCache.cli_dict][cli][DockerCache.xml]\n+        else:\n+            print('no cli named %s' % imgName)\n+            return None\n+\n+    def getCLIDict(self, imgName):","path":"server/docker_resource.py","position":373,"original_position":373,"commit_id":"63b1947da5078cbb50126f61b336cedf7be45cb1","original_commit_id":"63b1947da5078cbb50126f61b336cedf7be45cb1","user":{"login":"salamb","id":3301329,"avatar_url":"https://avatars.githubusercontent.com/u/3301329?v=3","gravatar_id":"","url":"https://api.github.com/users/salamb","html_url":"https://github.com/salamb","followers_url":"https://api.github.com/users/salamb/followers","following_url":"https://api.github.com/users/salamb/following{/other_user}","gists_url":"https://api.github.com/users/salamb/gists{/gist_id}","starred_url":"https://api.github.com/users/salamb/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/salamb/subscriptions","organizations_url":"https://api.github.com/users/salamb/orgs","repos_url":"https://api.github.com/users/salamb/repos","events_url":"https://api.github.com/users/salamb/events{/privacy}","received_events_url":"https://api.github.com/users/salamb/received_events","type":"User","site_admin":false},"body":"@cdeepakroy \r\nI will refactor the name, but it is returning the json structure defined for each image which is why I though calling it a dict will be more appropriate.  ","created_at":"2016-07-29T13:08:50Z","updated_at":"2016-07-29T13:08:50Z","html_url":"https://github.com/DigitalSlideArchive/HistomicsTK/pull/131#discussion_r72788058","pull_request_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/131","_links":{"self":{"href":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/comments/72788058"},"html":{"href":"https://github.com/DigitalSlideArchive/HistomicsTK/pull/131#discussion_r72788058"},"pull_request":{"href":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/131"}}},"pull_request":{"url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/131","id":78029587,"html_url":"https://github.com/DigitalSlideArchive/HistomicsTK/pull/131","diff_url":"https://github.com/DigitalSlideArchive/HistomicsTK/pull/131.diff","patch_url":"https://github.com/DigitalSlideArchive/HistomicsTK/pull/131.patch","issue_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues/131","number":131,"state":"open","locked":false,"title":"[WIP]Add docker images via settings","user":{"login":"salamb","id":3301329,"avatar_url":"https://avatars.githubusercontent.com/u/3301329?v=3","gravatar_id":"","url":"https://api.github.com/users/salamb","html_url":"https://github.com/salamb","followers_url":"https://api.github.com/users/salamb/followers","following_url":"https://api.github.com/users/salamb/following{/other_user}","gists_url":"https://api.github.com/users/salamb/gists{/gist_id}","starred_url":"https://api.github.com/users/salamb/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/salamb/subscriptions","organizations_url":"https://api.github.com/users/salamb/orgs","repos_url":"https://api.github.com/users/salamb/repos","events_url":"https://api.github.com/users/salamb/events{/privacy}","received_events_url":"https://api.github.com/users/salamb/received_events","type":"User","site_admin":false},"body":"@brianhelba @cdeepakroy \r\nThe key is histomicstk.docker_images and the respective value is a list of dictionaries.\r\nDue to the way settings are stored in girder, adding a docker image with a period causes a database issue.","created_at":"2016-07-19T18:54:33Z","updated_at":"2016-07-29T13:08:50Z","closed_at":null,"merged_at":null,"merge_commit_sha":"d25957ca4041469e2c690fa35c2e8181daf81d1e","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/131/commits","review_comments_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/131/comments","review_comment_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/comments{/number}","comments_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues/131/comments","statuses_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/statuses/63b1947da5078cbb50126f61b336cedf7be45cb1","head":{"label":"DigitalSlideArchive:add_docker_images_via_settings","ref":"add_docker_images_via_settings","sha":"63b1947da5078cbb50126f61b336cedf7be45cb1","user":{"login":"DigitalSlideArchive","id":13769637,"avatar_url":"https://avatars.githubusercontent.com/u/13769637?v=3","gravatar_id":"","url":"https://api.github.com/users/DigitalSlideArchive","html_url":"https://github.com/DigitalSlideArchive","followers_url":"https://api.github.com/users/DigitalSlideArchive/followers","following_url":"https://api.github.com/users/DigitalSlideArchive/following{/other_user}","gists_url":"https://api.github.com/users/DigitalSlideArchive/gists{/gist_id}","starred_url":"https://api.github.com/users/DigitalSlideArchive/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DigitalSlideArchive/subscriptions","organizations_url":"https://api.github.com/users/DigitalSlideArchive/orgs","repos_url":"https://api.github.com/users/DigitalSlideArchive/repos","events_url":"https://api.github.com/users/DigitalSlideArchive/events{/privacy}","received_events_url":"https://api.github.com/users/DigitalSlideArchive/received_events","type":"Organization","site_admin":false},"repo":{"id":44324447,"name":"HistomicsTK","full_name":"DigitalSlideArchive/HistomicsTK","owner":{"login":"DigitalSlideArchive","id":13769637,"avatar_url":"https://avatars.githubusercontent.com/u/13769637?v=3","gravatar_id":"","url":"https://api.github.com/users/DigitalSlideArchive","html_url":"https://github.com/DigitalSlideArchive","followers_url":"https://api.github.com/users/DigitalSlideArchive/followers","following_url":"https://api.github.com/users/DigitalSlideArchive/following{/other_user}","gists_url":"https://api.github.com/users/DigitalSlideArchive/gists{/gist_id}","starred_url":"https://api.github.com/users/DigitalSlideArchive/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DigitalSlideArchive/subscriptions","organizations_url":"https://api.github.com/users/DigitalSlideArchive/orgs","repos_url":"https://api.github.com/users/DigitalSlideArchive/repos","events_url":"https://api.github.com/users/DigitalSlideArchive/events{/privacy}","received_events_url":"https://api.github.com/users/DigitalSlideArchive/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/DigitalSlideArchive/HistomicsTK","description":"A Python and REST API for Histopathology Image Analysis","fork":false,"url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK","forks_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/forks","keys_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/keys{/key_id}","collaborators_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/teams","hooks_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/hooks","issue_events_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues/events{/number}","events_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/events","assignees_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/assignees{/user}","branches_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/branches{/branch}","tags_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/tags","blobs_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/refs{/sha}","trees_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/trees{/sha}","statuses_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/statuses/{sha}","languages_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/languages","stargazers_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/stargazers","contributors_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/contributors","subscribers_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/subscribers","subscription_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/subscription","commits_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/commits{/sha}","git_commits_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/commits{/sha}","comments_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/comments{/number}","issue_comment_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues/comments{/number}","contents_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/contents/{+path}","compare_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/compare/{base}...{head}","merges_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/merges","archive_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/downloads","issues_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues{/number}","pulls_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls{/number}","milestones_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/milestones{/number}","notifications_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/labels{/name}","releases_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/releases{/id}","deployments_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/deployments","created_at":"2015-10-15T14:49:21Z","updated_at":"2016-07-05T15:24:59Z","pushed_at":"2016-07-29T04:38:30Z","git_url":"git://github.com/DigitalSlideArchive/HistomicsTK.git","ssh_url":"git@github.com:DigitalSlideArchive/HistomicsTK.git","clone_url":"https://github.com/DigitalSlideArchive/HistomicsTK.git","svn_url":"https://github.com/DigitalSlideArchive/HistomicsTK","homepage":"","size":14238,"stargazers_count":7,"watchers_count":7,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":5,"mirror_url":null,"open_issues_count":23,"forks":5,"open_issues":23,"watchers":7,"default_branch":"master"}},"base":{"label":"DigitalSlideArchive:master","ref":"master","sha":"cd6dd569f48ffa962249c2c5f9453a1dd55127ae","user":{"login":"DigitalSlideArchive","id":13769637,"avatar_url":"https://avatars.githubusercontent.com/u/13769637?v=3","gravatar_id":"","url":"https://api.github.com/users/DigitalSlideArchive","html_url":"https://github.com/DigitalSlideArchive","followers_url":"https://api.github.com/users/DigitalSlideArchive/followers","following_url":"https://api.github.com/users/DigitalSlideArchive/following{/other_user}","gists_url":"https://api.github.com/users/DigitalSlideArchive/gists{/gist_id}","starred_url":"https://api.github.com/users/DigitalSlideArchive/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DigitalSlideArchive/subscriptions","organizations_url":"https://api.github.com/users/DigitalSlideArchive/orgs","repos_url":"https://api.github.com/users/DigitalSlideArchive/repos","events_url":"https://api.github.com/users/DigitalSlideArchive/events{/privacy}","received_events_url":"https://api.github.com/users/DigitalSlideArchive/received_events","type":"Organization","site_admin":false},"repo":{"id":44324447,"name":"HistomicsTK","full_name":"DigitalSlideArchive/HistomicsTK","owner":{"login":"DigitalSlideArchive","id":13769637,"avatar_url":"https://avatars.githubusercontent.com/u/13769637?v=3","gravatar_id":"","url":"https://api.github.com/users/DigitalSlideArchive","html_url":"https://github.com/DigitalSlideArchive","followers_url":"https://api.github.com/users/DigitalSlideArchive/followers","following_url":"https://api.github.com/users/DigitalSlideArchive/following{/other_user}","gists_url":"https://api.github.com/users/DigitalSlideArchive/gists{/gist_id}","starred_url":"https://api.github.com/users/DigitalSlideArchive/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/DigitalSlideArchive/subscriptions","organizations_url":"https://api.github.com/users/DigitalSlideArchive/orgs","repos_url":"https://api.github.com/users/DigitalSlideArchive/repos","events_url":"https://api.github.com/users/DigitalSlideArchive/events{/privacy}","received_events_url":"https://api.github.com/users/DigitalSlideArchive/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/DigitalSlideArchive/HistomicsTK","description":"A Python and REST API for Histopathology Image Analysis","fork":false,"url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK","forks_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/forks","keys_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/keys{/key_id}","collaborators_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/teams","hooks_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/hooks","issue_events_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues/events{/number}","events_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/events","assignees_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/assignees{/user}","branches_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/branches{/branch}","tags_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/tags","blobs_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/refs{/sha}","trees_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/trees{/sha}","statuses_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/statuses/{sha}","languages_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/languages","stargazers_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/stargazers","contributors_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/contributors","subscribers_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/subscribers","subscription_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/subscription","commits_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/commits{/sha}","git_commits_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/git/commits{/sha}","comments_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/comments{/number}","issue_comment_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues/comments{/number}","contents_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/contents/{+path}","compare_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/compare/{base}...{head}","merges_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/merges","archive_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/downloads","issues_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues{/number}","pulls_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls{/number}","milestones_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/milestones{/number}","notifications_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/labels{/name}","releases_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/releases{/id}","deployments_url":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/deployments","created_at":"2015-10-15T14:49:21Z","updated_at":"2016-07-05T15:24:59Z","pushed_at":"2016-07-29T04:38:30Z","git_url":"git://github.com/DigitalSlideArchive/HistomicsTK.git","ssh_url":"git@github.com:DigitalSlideArchive/HistomicsTK.git","clone_url":"https://github.com/DigitalSlideArchive/HistomicsTK.git","svn_url":"https://github.com/DigitalSlideArchive/HistomicsTK","homepage":"","size":14238,"stargazers_count":7,"watchers_count":7,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":5,"mirror_url":null,"open_issues_count":23,"forks":5,"open_issues":23,"watchers":7,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/131"},"html":{"href":"https://github.com/DigitalSlideArchive/HistomicsTK/pull/131"},"issue":{"href":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues/131"},"comments":{"href":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/issues/131/comments"},"review_comments":{"href":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/131/comments"},"review_comment":{"href":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/pulls/131/commits"},"statuses":{"href":"https://api.github.com/repos/DigitalSlideArchive/HistomicsTK/statuses/63b1947da5078cbb50126f61b336cedf7be45cb1"}}}}
{"actor":{"display_login":"salamb"}}