{"action":"created","comment":{"url":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/comments/53460000","id":53460000,"diff_hunk":"@@ -308,21 +314,206 @@ def filter_row(self, row):\n         return row\n \n \n-class CourseStructureTask(FileCopyMixin, BaseObfuscateDumpTask):\n-    \"\"\"Task to copy course_structure dump file, no obfuscation needed.\"\"\"\n+class XBlockConfigMixin(object):\n+\n+    xblock_obfuscation_config = luigi.Parameter(\n+        config_path={'section': 'obfuscation', 'name': 'xblock_obfuscation_config'}\n+    )\n+\n+    def requires(self):\n+        reqs = super(XBlockConfigMixin, self).requires()\n+        if os.path.basename(self.xblock_obfuscation_config) != self.xblock_obfuscation_config:\n+            reqs['xblock_config'] = ExternalURL(self.xblock_obfuscation_config)\n+        return reqs\n+\n+    @property\n+    def xblock_config(self):\n+        if not hasattr(self, '_xblock_config'):\n+            with read_config_file(self.xblock_obfuscation_config) as xblock_config_file:\n+                self._xblock_config = yaml.load(xblock_config_file)\n+\n+        return self._xblock_config\n+\n+    @property\n+    def field_lists(self):\n+        if not hasattr(self, '_field_lists'):\n+            self._field_lists = {}\n+            for block_type, block_config in self.xblock_config['xblocks'].iteritems():\n+                self._field_lists[block_type] = {\n+                    'whitelist': frozenset(block_config.get('fields', []) + self._xblock_config.get('fields', [])),\n+                    'blacklist': frozenset(block_config.get('exclude_fields', []) + self._xblock_config.get('exclude_fields', [])),\n+                }\n+\n+        return self._field_lists\n+\n+    def get_field_list(self, block_type, list_type):\n+        return self.field_lists.get(block_type, {}).get(list_type, frozenset())\n+\n+    def get_field_whitelist(self, block_type):\n+        return self.get_field_list(block_type, 'whitelist')\n+\n+    def get_field_blacklist(self, block_type):\n+        return self.get_field_list(block_type, 'blacklist')\n+\n+    def remove_fields_from_dict(self, block_type, dict_to_modify):\n+        to_remove = frozenset(dict_to_modify.keys()) - self.get_field_whitelist(block_type)\n+        for attribute in to_remove:\n+            dict_to_modify.pop(attribute)\n+            if attribute in self.get_field_blacklist(block_type):\n+                log.debug('Removed blacklisted attribute \"{0}\" from \"{1}\"'.format(attribute, block_type))\n+            else:\n+                log.warning('Removed unknown attribute \"{0}\" from \"{1}\"'.format(attribute, block_type))\n+\n+        return to_remove\n+\n+\n+class CourseStructureTask(XBlockConfigMixin, BaseObfuscateDumpTask):\n+    \"\"\"Task to copy course_structure dump file, removing any unknown elements and fields.\"\"\"\n \n     @property\n     def file_pattern(self):\n         return '*-course_structure-*'\n \n+    def run(self):\n+        with self.output().open('w') as output_file:\n+            with self.input()['data'][0].open('r') as input_file:\n+                course_structure = json.load(input_file)\n+                for block_id, block_structure in course_structure.iteritems():\n+                    category = block_structure.get('category')\n+                    metadata = block_structure.get('metadata', {})\n+                    if category in self.xblock_config['xblocks']:\n+                        removed_fields = self.remove_fields_from_dict(category, metadata)\n+                    else:\n+                        removed_fields = set(metadata.keys())\n+                        for key in removed_fields:\n+                            del metadata[key]\n+                        if category in self.xblock_config.get('exclude_xblocks', set()):\n+                            log.debug('Removed metadata from blacklisted block \"{0}\"'.format(category))\n+                        else:\n+                            log.warning('Removed metadata from unknown block \"{0}\"'.format(category))\n+\n+                    if len(removed_fields) > 0:\n+                        block_structure['redacted_metadata'] = list(removed_fields)\n \n-class CourseContentTask(FileCopyMixin, BaseObfuscateDumpTask):\n-    \"\"\"Task to copy course dump file, no obfuscation needed.\"\"\"\n+                json.dump(course_structure, output_file)\n+\n+\n+class CourseContentTask(XBlockConfigMixin, BaseObfuscateDumpTask):\n+    \"\"\"Task to copy course dump file, removing any unknown elements and fields.\"\"\"\n+\n+    XML_PACKAGE_REF_ATTRIBUTE = 'url_name'\n \n     @property\n     def file_pattern(self):\n         return '*-course-*'\n \n+    def run(self):\n+        with self.output().open('w') as output_file:\n+            with self.input()['data'][0].open('r') as input_file:\n+                with make_temp_directory(prefix='obfuscate-course.') as tmp_directory:\n+                    with tarfile.open(mode='r:gz', fileobj=input_file) as course_archive:\n+                        course_archive.extractall(tmp_directory)\n+\n+                    course_dir = os.listdir(tmp_directory)[0]\n+                    root_dir = os.path.join(tmp_directory, course_dir)\n+\n+                    self.clean_drafts(root_dir)\n+\n+                    course_url_name = self.read_course_url_name(root_dir)\n+\n+                    policy_file_path = os.path.join(root_dir, 'policies', course_url_name, 'policy.json')\n+                    self.clean_course_policy(course_url_name, policy_file_path)\n+\n+                    self.clean_xml_files(root_dir)\n+\n+                    with tarfile.open(mode='w:gz', fileobj=output_file) as output_archive_file:\n+                        output_archive_file.add(tmp_directory, arcname='')\n+\n+    def clean_drafts(self, root_dir):\n+        drafts_path = os.path.join(root_dir, 'drafts')\n+        if os.path.exists(drafts_path):\n+            log.debug('Removing all drafts')\n+            shutil.rmtree(drafts_path)\n+\n+    def read_course_url_name(self, root_dir):\n+        course_metadata_et = xml.etree.ElementTree.parse(os.path.join(root_dir, 'course.xml'))\n+        course_metadata = course_metadata_et.getroot()\n+        course_url_name = course_metadata.attrib.get('url_name')\n+        return course_url_name\n+\n+    def clean_course_policy(self, course_url_name, policy_file_path):\n+        log.debug('Cleaning course policy file \"{0}\"'.format(policy_file_path))\n+        with open(policy_file_path, 'r') as policy_file:\n+            policy = json.load(policy_file)\n+            course_root_policy = policy.get('course/' + course_url_name, {})\n+            removed_fields = self.remove_fields_from_dict('course', course_root_policy)\n+            course_root_policy['redacted_attributes'] = list(removed_fields)\n+        with open(policy_file_path, 'w') as policy_file:\n+            json.dump(policy, policy_file)\n+\n+    def clean_xml_files(self, root_dir):\n+        log.debug('Cleaning XML files')\n+        xml_file_paths = [target.path for target in PathSetTask([root_dir], ['*.xml']).output()]\n+        for xml_file_path in xml_file_paths:\n+            et = xml.etree.ElementTree.parse(xml_file_path)\n+            element = et.getroot()\n+            self.clean_element(element)\n+            et.write(xml_file_path)\n+\n+    def clean_element(self, element):\n+        block_config = self.xblock_config['xblocks'].get(element.tag)\n+        removed_fields = set()\n+        removed_children = set()\n+        if block_config:\n+            removed_fields = self.remove_fields_from_dict(element.tag, element.attrib)\n+\n+            # The logic here is a bit tricky, a sub-element can either be a field of the current block or a child block.\n+            # We just assume everything is a child block unless we are told otherwise by the configuration. Fields that\n+            # can appear as sub-elements need to be declared in the field whitelist. Note that some wonkiness is\n+            # possible if a field name happens to conflict with a block name. This could result in us thinking a child\n+            # block is actually a field and ignoring it.\n+            #\n+            # We only \"clean\" a sub-element if and only if:","path":"edx/analytics/tasks/data_obfuscation.py","position":200,"original_position":200,"commit_id":"d422aebe833d42985aaa4f17ad079d07891e2e03","original_commit_id":"acdde660511d98d2b1dabd76a0fd0a9a9d696721","user":{"login":"mulby","id":627177,"avatar_url":"https://avatars.githubusercontent.com/u/627177?v=3","gravatar_id":"","url":"https://api.github.com/users/mulby","html_url":"https://github.com/mulby","followers_url":"https://api.github.com/users/mulby/followers","following_url":"https://api.github.com/users/mulby/following{/other_user}","gists_url":"https://api.github.com/users/mulby/gists{/gist_id}","starred_url":"https://api.github.com/users/mulby/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mulby/subscriptions","organizations_url":"https://api.github.com/users/mulby/orgs","repos_url":"https://api.github.com/users/mulby/repos","events_url":"https://api.github.com/users/mulby/events{/privacy}","received_events_url":"https://api.github.com/users/mulby/received_events","type":"User","site_admin":false},"body":"done","created_at":"2016-02-19T13:28:04Z","updated_at":"2016-02-19T13:28:04Z","html_url":"https://github.com/edx/edx-analytics-pipeline/pull/203#discussion_r53460000","pull_request_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/203","_links":{"self":{"href":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/comments/53460000"},"html":{"href":"https://github.com/edx/edx-analytics-pipeline/pull/203#discussion_r53460000"},"pull_request":{"href":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/203"}}},"pull_request":{"url":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/203","id":59865759,"html_url":"https://github.com/edx/edx-analytics-pipeline/pull/203","diff_url":"https://github.com/edx/edx-analytics-pipeline/pull/203.diff","patch_url":"https://github.com/edx/edx-analytics-pipeline/pull/203.patch","issue_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues/203","number":203,"state":"open","locked":false,"title":"Remove potentially sensitive XBlock fields from obfuscated data packages","user":{"login":"mulby","id":627177,"avatar_url":"https://avatars.githubusercontent.com/u/627177?v=3","gravatar_id":"","url":"https://api.github.com/users/mulby","html_url":"https://github.com/mulby","followers_url":"https://api.github.com/users/mulby/followers","following_url":"https://api.github.com/users/mulby/following{/other_user}","gists_url":"https://api.github.com/users/mulby/gists{/gist_id}","starred_url":"https://api.github.com/users/mulby/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mulby/subscriptions","organizations_url":"https://api.github.com/users/mulby/orgs","repos_url":"https://api.github.com/users/mulby/repos","events_url":"https://api.github.com/users/mulby/events{/privacy}","received_events_url":"https://api.github.com/users/mulby/received_events","type":"User","site_admin":false},"body":"@brianhw this is my PR rebased on master, I think I addressed most of your feedback. Going to do a pep8/pylint pass now.","created_at":"2016-02-18T21:25:35Z","updated_at":"2016-02-19T13:28:04Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4c6a13f62e14619651799fec9bcb98b9f20b6690","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/203/commits","review_comments_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/203/comments","review_comment_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/comments{/number}","comments_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues/203/comments","statuses_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/statuses/d422aebe833d42985aaa4f17ad079d07891e2e03","head":{"label":"edx:gabe/remove-xblock-fields","ref":"gabe/remove-xblock-fields","sha":"d422aebe833d42985aaa4f17ad079d07891e2e03","user":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"repo":{"id":22929261,"name":"edx-analytics-pipeline","full_name":"edx/edx-analytics-pipeline","owner":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/edx/edx-analytics-pipeline","description":"","fork":false,"url":"https://api.github.com/repos/edx/edx-analytics-pipeline","forks_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/forks","keys_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/keys{/key_id}","collaborators_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/teams","hooks_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/hooks","issue_events_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues/events{/number}","events_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/events","assignees_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/assignees{/user}","branches_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/branches{/branch}","tags_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/tags","blobs_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/refs{/sha}","trees_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/trees{/sha}","statuses_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/statuses/{sha}","languages_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/languages","stargazers_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/stargazers","contributors_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/contributors","subscribers_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/subscribers","subscription_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/subscription","commits_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/commits{/sha}","git_commits_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/commits{/sha}","comments_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/comments{/number}","issue_comment_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues/comments{/number}","contents_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/contents/{+path}","compare_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/compare/{base}...{head}","merges_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/merges","archive_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/downloads","issues_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues{/number}","pulls_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls{/number}","milestones_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/milestones{/number}","notifications_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/labels{/name}","releases_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/releases{/id}","deployments_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/deployments","created_at":"2014-08-13T19:57:17Z","updated_at":"2016-02-18T21:28:27Z","pushed_at":"2016-02-19T10:41:15Z","git_url":"git://github.com/edx/edx-analytics-pipeline.git","ssh_url":"git@github.com:edx/edx-analytics-pipeline.git","clone_url":"https://github.com/edx/edx-analytics-pipeline.git","svn_url":"https://github.com/edx/edx-analytics-pipeline","homepage":null,"size":2964,"stargazers_count":19,"watchers_count":19,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":32,"mirror_url":null,"open_issues_count":12,"forks":32,"open_issues":12,"watchers":19,"default_branch":"master"}},"base":{"label":"edx:master","ref":"master","sha":"0d1b751b89f07189a4cbdaa6bb7428540ae3ac8c","user":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"repo":{"id":22929261,"name":"edx-analytics-pipeline","full_name":"edx/edx-analytics-pipeline","owner":{"login":"edx","id":3179841,"avatar_url":"https://avatars.githubusercontent.com/u/3179841?v=3","gravatar_id":"","url":"https://api.github.com/users/edx","html_url":"https://github.com/edx","followers_url":"https://api.github.com/users/edx/followers","following_url":"https://api.github.com/users/edx/following{/other_user}","gists_url":"https://api.github.com/users/edx/gists{/gist_id}","starred_url":"https://api.github.com/users/edx/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/edx/subscriptions","organizations_url":"https://api.github.com/users/edx/orgs","repos_url":"https://api.github.com/users/edx/repos","events_url":"https://api.github.com/users/edx/events{/privacy}","received_events_url":"https://api.github.com/users/edx/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/edx/edx-analytics-pipeline","description":"","fork":false,"url":"https://api.github.com/repos/edx/edx-analytics-pipeline","forks_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/forks","keys_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/keys{/key_id}","collaborators_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/teams","hooks_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/hooks","issue_events_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues/events{/number}","events_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/events","assignees_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/assignees{/user}","branches_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/branches{/branch}","tags_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/tags","blobs_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/refs{/sha}","trees_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/trees{/sha}","statuses_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/statuses/{sha}","languages_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/languages","stargazers_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/stargazers","contributors_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/contributors","subscribers_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/subscribers","subscription_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/subscription","commits_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/commits{/sha}","git_commits_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/git/commits{/sha}","comments_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/comments{/number}","issue_comment_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues/comments{/number}","contents_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/contents/{+path}","compare_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/compare/{base}...{head}","merges_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/merges","archive_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/downloads","issues_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues{/number}","pulls_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls{/number}","milestones_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/milestones{/number}","notifications_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/labels{/name}","releases_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/releases{/id}","deployments_url":"https://api.github.com/repos/edx/edx-analytics-pipeline/deployments","created_at":"2014-08-13T19:57:17Z","updated_at":"2016-02-18T21:28:27Z","pushed_at":"2016-02-19T10:41:15Z","git_url":"git://github.com/edx/edx-analytics-pipeline.git","ssh_url":"git@github.com:edx/edx-analytics-pipeline.git","clone_url":"https://github.com/edx/edx-analytics-pipeline.git","svn_url":"https://github.com/edx/edx-analytics-pipeline","homepage":null,"size":2964,"stargazers_count":19,"watchers_count":19,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":32,"mirror_url":null,"open_issues_count":12,"forks":32,"open_issues":12,"watchers":19,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/203"},"html":{"href":"https://github.com/edx/edx-analytics-pipeline/pull/203"},"issue":{"href":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues/203"},"comments":{"href":"https://api.github.com/repos/edx/edx-analytics-pipeline/issues/203/comments"},"review_comments":{"href":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/203/comments"},"review_comment":{"href":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/edx/edx-analytics-pipeline/pulls/203/commits"},"statuses":{"href":"https://api.github.com/repos/edx/edx-analytics-pipeline/statuses/d422aebe833d42985aaa4f17ad079d07891e2e03"}}}}