{"action":"created","comment":{"url":"https://api.github.com/repos/codalab/codalab-cli/pulls/comments/58580291","id":58580291,"diff_hunk":"@@ -0,0 +1,187 @@\n+import re\n+import os\n+import shutil\n+\n+from codalab.lib import crypt_util, file_util, path_util, zip_util\n+\n+class UploadManager(object):\n+    \"\"\"\n+    Contains logic for uploading bundle data to the bundle store and updating\n+    the associated bundle metadata in the database.\n+    \"\"\"\n+    def __init__(self, bundle_model, bundle_store):\n+        self._bundle_model = bundle_model\n+        self._bundle_store = bundle_store\n+\n+    def upload_to_bundle_store(self, bundle, sources, follow_symlinks, exclude_patterns, remove_sources, git, unpack, simplify_archives):\n+        \"\"\"\n+        Uploads contents for the given bundle to the bundle store.\n+\n+        |sources|: specifies the locations of the contents to upload. Each element is\n+                   either a URL, a local path or a tuple (filename, file-like object).\n+        |follow_symlinks|: for local path(s), whether to follow (resolve) symlinks,\n+                           but only if remove_sources is False.\n+        |exclude_patterns|: for local path(s), don't upload these patterns (e.g., *.o),\n+                            but only if remove_sources is False.\n+        |remove_sources|: for local path(s), whether |sources| should be removed\n+        |git|: for URLs, whether |source| is a git repo to clone.\n+        |unpack|: for each source in |sources|, whether to unpack it if it's an archive.\n+        |simplify_archives|: whether to simplify unpacked archives so that if they\n+                             contain a single file, the final path is just that file,\n+                             not a directory containing that file.\n+    \n+        If |sources| contains one source, then the bundle contents will be that source.\n+        Otherwise, the bundle contents will be a directory with each of the sources.\n+        Exceptions:\n+        - If |git|, then each source is replaced with the result of running 'git clone |source|'\n+        - If |unpack| is True or a source is an archive (zip, tar.gz, etc.), then unpack the source.\n+        \"\"\"\n+        bundle_path = self._bundle_store.get_bundle_location(bundle.uuid)\n+        try:\n+            path_util.make_directory(bundle_path)\n+            for source in sources:\n+                is_url, is_local_path, is_fileobj, filename = self._interpret_source(source)\n+                source_output_path = os.path.join(bundle_path, filename)\n+    \n+                if is_url:\n+                    if git:\n+                        source_output_path = file_util.strip_git_ext(source_output_path)\n+                        file_util.git_clone(source, source_output_path)\n+                    else:\n+                        file_util.download_url(source, source_output_path)\n+                        if unpack and self._can_unpack_file(source_output_path):\n+                            self._unpack_file(\n+                                source_output_path, zip_util.strip_archive_ext(source_output_path),\n+                                remove_source=True, simplify_archive=simplify_archives)\n+                elif is_local_path:\n+                    source_path = path_util.normalize(source)\n+                    path_util.check_isvalid(source_path, 'upload')\n+                    \n+                    if unpack and self._can_unpack_file(source_path):\n+                        self._unpack_file(\n+                            source_path, zip_util.strip_archive_ext(source_output_path),\n+                            remove_source=remove_sources, simplify_archive=simplify_archives)\n+                    elif remove_sources:\n+                        path_util.rename(source_path, source_output_path)\n+                    else:\n+                        path_util.copy(source_path, source_output_path, follow_symlinks=follow_symlinks, exclude_patterns=exclude_patterns)\n+                elif is_fileobj:\n+                    if unpack and zip_util.path_is_archive(filename):\n+                        self._unpack_fileobj(\n+                            source[0], source[1],\n+                            zip_util.strip_archive_ext(source_output_path),\n+                            simplify_archive=simplify_archives)\n+                    else:\n+                        with open(source_output_path, 'wb') as out:\n+                            shutil.copyfileobj(source[1], out)\n+\n+            if len(sources) == 1:\n+                self._simplify_directory(bundle_path)\n+        except:\n+            if os.path.exists(bundle_path):\n+                path_util.remove(bundle_path)\n+            raise\n+    \n+    def _interpret_source(self, source):\n+        is_url, is_local_path, is_fileobj = False, False, False\n+        if isinstance(source, basestring):\n+            if path_util.path_is_url(source):\n+                is_url = True\n+            else:\n+                is_local_path = True\n+            filename = os.path.basename(os.path.normpath(source))\n+        else:\n+            is_fileobj = True\n+            filename = source[0]\n+        return is_url, is_local_path, is_fileobj, filename\n+\n+    def _can_unpack_file(self, path):\n+        return os.path.isfile(path) and zip_util.path_is_archive(path)\n+    \n+    def _unpack_file(self, source_path, dest_path, remove_source, simplify_archive):\n+        zip_util.unpack(zip_util.get_archive_ext(source_path), source_path, dest_path)\n+        if remove_source:\n+            path_util.remove(source_path)\n+        if simplify_archive:\n+            self._simplify_archive(dest_path)\n+\n+    def _unpack_fileobj(self, source_filename, source_fileobj, dest_path, simplify_archive):\n+        zip_util.unpack(zip_util.get_archive_ext(source_filename), source_fileobj, dest_path)\n+        if simplify_archive:\n+            self._simplify_archive(dest_path)\n+\n+    def _simplify_archive(self, path):\n+        if not os.path.isdir(path):\n+            return\n+\n+        files = [f for f in os.listdir(path) if not self._ignore_file_in_archive(f)]\n+        if len(files) == 1:\n+            self._simplify_directory(path, files[0])\n+\n+    # ... ignore files that match any of these exactly\n+    IGNORE_FILE_EXACT = ['.DS_Store', '__MACOSX']\n+    # ... ignore files that match any of these patterns\n+    IGNORE_FILE_PATTERNS = [re.compile(s) for s in ['^\\._.*']]\n+\n+    def _ignore_file_in_archive(self, filename):\n+        if filename in self.IGNORE_FILE_EXACT:\n+            return True\n+        return any([pattern.match(filename) for pattern in self.IGNORE_FILE_PATTERNS])\n+\n+    def _simplify_directory(self, path, child_path=None):\n+        if child_path is None:\n+            child_path = os.listdir(path)[0]\n+\n+        temp_path = path + crypt_util.get_random_string()\n+        path_util.rename(path, temp_path)\n+        child_path = os.path.join(temp_path, child_path)\n+        path_util.rename(child_path, path)\n+        path_util.remove(temp_path)\n+\n+    def update_metadata_and_save(self, bundle, new_bundle):\n+        \"\"\"\n+        Updates the metadata about the contents of the bundle, including\n+        data_size as well as the total amount of disk used by the user.\n+\n+        If |new_bundle| is True, saves the bundle as a new bundle. Otherwise,\n+        updates it.\n+        \"\"\"\n+        bundle_path = self._bundle_store.get_bundle_location(bundle.uuid)\n+\n+        dirs_and_files = None\n+        if os.path.isdir(bundle_path):\n+            dirs_and_files = path_util.recursive_ls(bundle_path)\n+        else:\n+            dirs_and_files = [], [bundle_path]\n+\n+        data_hash = '0x%s' % (path_util.hash_directory(bundle_path, dirs_and_files))\n+        data_size = path_util.get_size(bundle_path, dirs_and_files)\n+\n+        if new_bundle:\n+            bundle.data_hash = data_hash\n+            bundle.metadata.set_metadata_key('data_size', data_size)\n+            self._bundle_model.save_bundle(bundle)\n+        else:\n+            bundle_update = {\n+               'data_hash': data_hash,\n+               'metadata': {\n+                    'data_size': data_size,             \n+                },\n+            }\n+            self._bundle_model.update_bundle(bundle, bundle_update)\n+\n+        self._bundle_model.update_user_disk_used(bundle.owner_id)\n+\n+    def has_contents(self, bundle):\n+        return os.path.exists(self._bundle_store.get_bundle_location(bundle.uuid))\n+\n+    def cleanup_existing_contents(self, bundle):\n+        self._bundle_store.cleanup(bundle.uuid, dry_run=False)","path":"codalab/lib/upload_manager.py","position":179,"original_position":179,"commit_id":"ade2e4092394d97bd4423a2193073fe9e2387f48","original_commit_id":"ade2e4092394d97bd4423a2193073fe9e2387f48","user":{"login":"percyliang","id":435384,"avatar_url":"https://avatars.githubusercontent.com/u/435384?v=3","gravatar_id":"","url":"https://api.github.com/users/percyliang","html_url":"https://github.com/percyliang","followers_url":"https://api.github.com/users/percyliang/followers","following_url":"https://api.github.com/users/percyliang/following{/other_user}","gists_url":"https://api.github.com/users/percyliang/gists{/gist_id}","starred_url":"https://api.github.com/users/percyliang/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/percyliang/subscriptions","organizations_url":"https://api.github.com/users/percyliang/orgs","repos_url":"https://api.github.com/users/percyliang/repos","events_url":"https://api.github.com/users/percyliang/events{/privacy}","received_events_url":"https://api.github.com/users/percyliang/received_events","type":"User","site_admin":false},"body":"When is this called?  If we're deleting the bundle, then why update the bundle metadata?","created_at":"2016-04-05T17:30:08Z","updated_at":"2016-04-05T17:30:08Z","html_url":"https://github.com/codalab/codalab-cli/pull/404#discussion_r58580291","pull_request_url":"https://api.github.com/repos/codalab/codalab-cli/pulls/404","_links":{"self":{"href":"https://api.github.com/repos/codalab/codalab-cli/pulls/comments/58580291"},"html":{"href":"https://github.com/codalab/codalab-cli/pull/404#discussion_r58580291"},"pull_request":{"href":"https://api.github.com/repos/codalab/codalab-cli/pulls/404"}}},"pull_request":{"url":"https://api.github.com/repos/codalab/codalab-cli/pulls/404","id":63012239,"html_url":"https://github.com/codalab/codalab-cli/pull/404","diff_url":"https://github.com/codalab/codalab-cli/pull/404.diff","patch_url":"https://github.com/codalab/codalab-cli/pull/404.patch","issue_url":"https://api.github.com/repos/codalab/codalab-cli/issues/404","number":404,"state":"open","locked":false,"title":"Refactoring uploading code.","user":{"login":"klopyrev","id":5249090,"avatar_url":"https://avatars.githubusercontent.com/u/5249090?v=3","gravatar_id":"","url":"https://api.github.com/users/klopyrev","html_url":"https://github.com/klopyrev","followers_url":"https://api.github.com/users/klopyrev/followers","following_url":"https://api.github.com/users/klopyrev/following{/other_user}","gists_url":"https://api.github.com/users/klopyrev/gists{/gist_id}","starred_url":"https://api.github.com/users/klopyrev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/klopyrev/subscriptions","organizations_url":"https://api.github.com/users/klopyrev/orgs","repos_url":"https://api.github.com/users/klopyrev/repos","events_url":"https://api.github.com/users/klopyrev/events{/privacy}","received_events_url":"https://api.github.com/users/klopyrev/received_events","type":"User","site_admin":false},"body":"Refactor uploading logic to:\r\n\r\n1) Support streaming archives in and add an API to be used by the worker for updating the contents of a bundle. Most of the same logic can be used for the upload REST API.\r\n2) Minimize the use of temporary files.\r\n3) Use the same logic for updating bundles as well for uploading new bundles.\r\n4) Clean up better on failed uploads.\r\n\r\nAlso, I centralize things more in the upload manager class.\r\n\r\n@andreweduffy @percyliang \r\n","created_at":"2016-03-16T04:17:47Z","updated_at":"2016-04-05T17:30:08Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4d4e866a595b892f634d1fd3c21c07e12798f7a3","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/codalab/codalab-cli/pulls/404/commits","review_comments_url":"https://api.github.com/repos/codalab/codalab-cli/pulls/404/comments","review_comment_url":"https://api.github.com/repos/codalab/codalab-cli/pulls/comments{/number}","comments_url":"https://api.github.com/repos/codalab/codalab-cli/issues/404/comments","statuses_url":"https://api.github.com/repos/codalab/codalab-cli/statuses/ade2e4092394d97bd4423a2193073fe9e2387f48","head":{"label":"klopyrev:refactor_uploading","ref":"refactor_uploading","sha":"ade2e4092394d97bd4423a2193073fe9e2387f48","user":{"login":"klopyrev","id":5249090,"avatar_url":"https://avatars.githubusercontent.com/u/5249090?v=3","gravatar_id":"","url":"https://api.github.com/users/klopyrev","html_url":"https://github.com/klopyrev","followers_url":"https://api.github.com/users/klopyrev/followers","following_url":"https://api.github.com/users/klopyrev/following{/other_user}","gists_url":"https://api.github.com/users/klopyrev/gists{/gist_id}","starred_url":"https://api.github.com/users/klopyrev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/klopyrev/subscriptions","organizations_url":"https://api.github.com/users/klopyrev/orgs","repos_url":"https://api.github.com/users/klopyrev/repos","events_url":"https://api.github.com/users/klopyrev/events{/privacy}","received_events_url":"https://api.github.com/users/klopyrev/received_events","type":"User","site_admin":false},"repo":{"id":49246702,"name":"codalab-cli","full_name":"klopyrev/codalab-cli","owner":{"login":"klopyrev","id":5249090,"avatar_url":"https://avatars.githubusercontent.com/u/5249090?v=3","gravatar_id":"","url":"https://api.github.com/users/klopyrev","html_url":"https://github.com/klopyrev","followers_url":"https://api.github.com/users/klopyrev/followers","following_url":"https://api.github.com/users/klopyrev/following{/other_user}","gists_url":"https://api.github.com/users/klopyrev/gists{/gist_id}","starred_url":"https://api.github.com/users/klopyrev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/klopyrev/subscriptions","organizations_url":"https://api.github.com/users/klopyrev/orgs","repos_url":"https://api.github.com/users/klopyrev/repos","events_url":"https://api.github.com/users/klopyrev/events{/privacy}","received_events_url":"https://api.github.com/users/klopyrev/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/klopyrev/codalab-cli","description":"Bundle service and command-line interface for supporting worksheets in CodaLab.","fork":true,"url":"https://api.github.com/repos/klopyrev/codalab-cli","forks_url":"https://api.github.com/repos/klopyrev/codalab-cli/forks","keys_url":"https://api.github.com/repos/klopyrev/codalab-cli/keys{/key_id}","collaborators_url":"https://api.github.com/repos/klopyrev/codalab-cli/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/klopyrev/codalab-cli/teams","hooks_url":"https://api.github.com/repos/klopyrev/codalab-cli/hooks","issue_events_url":"https://api.github.com/repos/klopyrev/codalab-cli/issues/events{/number}","events_url":"https://api.github.com/repos/klopyrev/codalab-cli/events","assignees_url":"https://api.github.com/repos/klopyrev/codalab-cli/assignees{/user}","branches_url":"https://api.github.com/repos/klopyrev/codalab-cli/branches{/branch}","tags_url":"https://api.github.com/repos/klopyrev/codalab-cli/tags","blobs_url":"https://api.github.com/repos/klopyrev/codalab-cli/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/klopyrev/codalab-cli/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/klopyrev/codalab-cli/git/refs{/sha}","trees_url":"https://api.github.com/repos/klopyrev/codalab-cli/git/trees{/sha}","statuses_url":"https://api.github.com/repos/klopyrev/codalab-cli/statuses/{sha}","languages_url":"https://api.github.com/repos/klopyrev/codalab-cli/languages","stargazers_url":"https://api.github.com/repos/klopyrev/codalab-cli/stargazers","contributors_url":"https://api.github.com/repos/klopyrev/codalab-cli/contributors","subscribers_url":"https://api.github.com/repos/klopyrev/codalab-cli/subscribers","subscription_url":"https://api.github.com/repos/klopyrev/codalab-cli/subscription","commits_url":"https://api.github.com/repos/klopyrev/codalab-cli/commits{/sha}","git_commits_url":"https://api.github.com/repos/klopyrev/codalab-cli/git/commits{/sha}","comments_url":"https://api.github.com/repos/klopyrev/codalab-cli/comments{/number}","issue_comment_url":"https://api.github.com/repos/klopyrev/codalab-cli/issues/comments{/number}","contents_url":"https://api.github.com/repos/klopyrev/codalab-cli/contents/{+path}","compare_url":"https://api.github.com/repos/klopyrev/codalab-cli/compare/{base}...{head}","merges_url":"https://api.github.com/repos/klopyrev/codalab-cli/merges","archive_url":"https://api.github.com/repos/klopyrev/codalab-cli/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/klopyrev/codalab-cli/downloads","issues_url":"https://api.github.com/repos/klopyrev/codalab-cli/issues{/number}","pulls_url":"https://api.github.com/repos/klopyrev/codalab-cli/pulls{/number}","milestones_url":"https://api.github.com/repos/klopyrev/codalab-cli/milestones{/number}","notifications_url":"https://api.github.com/repos/klopyrev/codalab-cli/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/klopyrev/codalab-cli/labels{/name}","releases_url":"https://api.github.com/repos/klopyrev/codalab-cli/releases{/id}","deployments_url":"https://api.github.com/repos/klopyrev/codalab-cli/deployments","created_at":"2016-01-08T03:23:44Z","updated_at":"2016-02-11T03:55:46Z","pushed_at":"2016-04-05T01:04:55Z","git_url":"git://github.com/klopyrev/codalab-cli.git","ssh_url":"git@github.com:klopyrev/codalab-cli.git","clone_url":"https://github.com/klopyrev/codalab-cli.git","svn_url":"https://github.com/klopyrev/codalab-cli","homepage":null,"size":2582,"stargazers_count":1,"watchers_count":1,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":1,"default_branch":"master"}},"base":{"label":"codalab:develop","ref":"develop","sha":"b8cb6e80c55f93da3d6a06a45dc67a16d2e21ae8","user":{"login":"codalab","id":4633552,"avatar_url":"https://avatars.githubusercontent.com/u/4633552?v=3","gravatar_id":"","url":"https://api.github.com/users/codalab","html_url":"https://github.com/codalab","followers_url":"https://api.github.com/users/codalab/followers","following_url":"https://api.github.com/users/codalab/following{/other_user}","gists_url":"https://api.github.com/users/codalab/gists{/gist_id}","starred_url":"https://api.github.com/users/codalab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codalab/subscriptions","organizations_url":"https://api.github.com/users/codalab/orgs","repos_url":"https://api.github.com/users/codalab/repos","events_url":"https://api.github.com/users/codalab/events{/privacy}","received_events_url":"https://api.github.com/users/codalab/received_events","type":"User","site_admin":false},"repo":{"id":27352490,"name":"codalab-cli","full_name":"codalab/codalab-cli","owner":{"login":"codalab","id":4633552,"avatar_url":"https://avatars.githubusercontent.com/u/4633552?v=3","gravatar_id":"","url":"https://api.github.com/users/codalab","html_url":"https://github.com/codalab","followers_url":"https://api.github.com/users/codalab/followers","following_url":"https://api.github.com/users/codalab/following{/other_user}","gists_url":"https://api.github.com/users/codalab/gists{/gist_id}","starred_url":"https://api.github.com/users/codalab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/codalab/subscriptions","organizations_url":"https://api.github.com/users/codalab/orgs","repos_url":"https://api.github.com/users/codalab/repos","events_url":"https://api.github.com/users/codalab/events{/privacy}","received_events_url":"https://api.github.com/users/codalab/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/codalab/codalab-cli","description":"Bundle service and command-line interface for supporting worksheets in CodaLab.","fork":false,"url":"https://api.github.com/repos/codalab/codalab-cli","forks_url":"https://api.github.com/repos/codalab/codalab-cli/forks","keys_url":"https://api.github.com/repos/codalab/codalab-cli/keys{/key_id}","collaborators_url":"https://api.github.com/repos/codalab/codalab-cli/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/codalab/codalab-cli/teams","hooks_url":"https://api.github.com/repos/codalab/codalab-cli/hooks","issue_events_url":"https://api.github.com/repos/codalab/codalab-cli/issues/events{/number}","events_url":"https://api.github.com/repos/codalab/codalab-cli/events","assignees_url":"https://api.github.com/repos/codalab/codalab-cli/assignees{/user}","branches_url":"https://api.github.com/repos/codalab/codalab-cli/branches{/branch}","tags_url":"https://api.github.com/repos/codalab/codalab-cli/tags","blobs_url":"https://api.github.com/repos/codalab/codalab-cli/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/codalab/codalab-cli/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/codalab/codalab-cli/git/refs{/sha}","trees_url":"https://api.github.com/repos/codalab/codalab-cli/git/trees{/sha}","statuses_url":"https://api.github.com/repos/codalab/codalab-cli/statuses/{sha}","languages_url":"https://api.github.com/repos/codalab/codalab-cli/languages","stargazers_url":"https://api.github.com/repos/codalab/codalab-cli/stargazers","contributors_url":"https://api.github.com/repos/codalab/codalab-cli/contributors","subscribers_url":"https://api.github.com/repos/codalab/codalab-cli/subscribers","subscription_url":"https://api.github.com/repos/codalab/codalab-cli/subscription","commits_url":"https://api.github.com/repos/codalab/codalab-cli/commits{/sha}","git_commits_url":"https://api.github.com/repos/codalab/codalab-cli/git/commits{/sha}","comments_url":"https://api.github.com/repos/codalab/codalab-cli/comments{/number}","issue_comment_url":"https://api.github.com/repos/codalab/codalab-cli/issues/comments{/number}","contents_url":"https://api.github.com/repos/codalab/codalab-cli/contents/{+path}","compare_url":"https://api.github.com/repos/codalab/codalab-cli/compare/{base}...{head}","merges_url":"https://api.github.com/repos/codalab/codalab-cli/merges","archive_url":"https://api.github.com/repos/codalab/codalab-cli/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/codalab/codalab-cli/downloads","issues_url":"https://api.github.com/repos/codalab/codalab-cli/issues{/number}","pulls_url":"https://api.github.com/repos/codalab/codalab-cli/pulls{/number}","milestones_url":"https://api.github.com/repos/codalab/codalab-cli/milestones{/number}","notifications_url":"https://api.github.com/repos/codalab/codalab-cli/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/codalab/codalab-cli/labels{/name}","releases_url":"https://api.github.com/repos/codalab/codalab-cli/releases{/id}","deployments_url":"https://api.github.com/repos/codalab/codalab-cli/deployments","created_at":"2014-11-30T22:33:18Z","updated_at":"2016-01-19T21:16:59Z","pushed_at":"2016-04-05T01:04:56Z","git_url":"git://github.com/codalab/codalab-cli.git","ssh_url":"git@github.com:codalab/codalab-cli.git","clone_url":"https://github.com/codalab/codalab-cli.git","svn_url":"https://github.com/codalab/codalab-cli","homepage":null,"size":2609,"stargazers_count":10,"watchers_count":10,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":12,"mirror_url":null,"open_issues_count":60,"forks":12,"open_issues":60,"watchers":10,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/codalab/codalab-cli/pulls/404"},"html":{"href":"https://github.com/codalab/codalab-cli/pull/404"},"issue":{"href":"https://api.github.com/repos/codalab/codalab-cli/issues/404"},"comments":{"href":"https://api.github.com/repos/codalab/codalab-cli/issues/404/comments"},"review_comments":{"href":"https://api.github.com/repos/codalab/codalab-cli/pulls/404/comments"},"review_comment":{"href":"https://api.github.com/repos/codalab/codalab-cli/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/codalab/codalab-cli/pulls/404/commits"},"statuses":{"href":"https://api.github.com/repos/codalab/codalab-cli/statuses/ade2e4092394d97bd4423a2193073fe9e2387f48"}}}}