{"action":"created","comment":{"url":"https://api.github.com/repos/bokeh/bokeh/pulls/comments/76062460","id":76062460,"diff_hunk":"@@ -0,0 +1,232 @@\n+import ast, os, copy\n+\n+__all__ = [\"api_crawler\", \"differ\"]\n+\n+\n+class APICrawler(object):\n+    \"\"\"\n+    Api Crawler: Given a directory, it will use AST to crawl through each of the\n+    File and produce AST nodes, which can then be parsed into a representation\n+    of the structure of that file. The execution...\n+    \"\"\"\n+\n+    # Exclude these folders from search.\n+    exclude = (\"tests\", \"static\", \"sampledata\", \"mplexplorer\")\n+\n+\n+    # Point the crawler at this directory.\n+    def __init__(self, directory):\n+        self.directory = directory\n+\n+    def is_public(self, name):\n+        # Determines whether a given function or class is public or private. Names\n+        # with underscores are not allowed unless the name is \"__init__\"\n+        return not name.startswith(\"_\", 0, 1) or name == \"__init__\"\n+\n+    def is_function(self, ast_node):\n+        # Checks whether a node is a top level function. Checks whether it is an AST\n+        # function instance. Also uses column offset, i.e. whether the function has\n+        # any tabs or spaces before it.\n+        return isinstance(ast_node, ast.FunctionDef) and ast_node.col_offset == 0\n+\n+    def is_class(self, ast_node):\n+        # Checks whether a node is an instance of an ast Class\n+        return isinstance(ast_node, ast.ClassDef)\n+\n+    def get_classes(self, source):\n+        # For a given source, walk through the file looking for classes, and assign\n+        # the associated methods. Pass them as a dictionary containing each class\n+        # and its methods.\n+        parsed = ast.parse(source)\n+        classes = [node for node in ast.walk(parsed) if self.is_class(node) and self.is_public(node.name)]\n+        class_defs = {}\n+        for x in classes:\n+            class_defs[x.name] = {}\n+            methods = []\n+            for y in x.body:\n+                if isinstance(y, ast.FunctionDef) and self.is_public(y.name):\n+                    methods.append(y.name)\n+            class_defs[x.name][\"methods\"] = methods\n+        return class_defs\n+\n+    def get_functions(self, source):\n+        # For a given source, look for functions and return those functions as a list.\n+        parsed = ast.parse(source)\n+        functions = [node.name for node in ast.walk(parsed) if self.is_function(node) and self.is_public(node.name)]\n+        return functions\n+\n+    def get_filenames(self, directory):\n+        # Go through the file tree and grab the filnames for each file found.\n+        # Assign these to an array and return that array.\n+        files = []\n+        tree = os.walk(directory, topdown=True, followlinks=False)\n+        for dirpath, dirnames, filenames in tree:\n+            for folder in self.exclude:\n+                if folder is dirpath:\n+                    break\n+            else:\n+                for name in filenames:\n+                    if name.endswith(\".py\") and not name.startswith(\"_\"):\n+                        files.append(os.path.join(dirpath, name))\n+        return files\n+\n+    def get_files_dict(self, filenames):\n+        # Using a list of filenames, open each file and parse functions and classes\n+        # out of the source file.\n+        files_dict = {}\n+        for x in filenames:\n+            with open(x, \"r\") as f:\n+                source = f.read()\n+                files_dict[x] = {\"classes\": {}, \"functions\": []}\n+                files_dict[x][\"classes\"] = self.get_classes(source)\n+                files_dict[x][\"functions\"] = self.get_functions(source)\n+        return files_dict\n+\n+    def get_crawl_dict(self):\n+        # Generate the crawl information using get_filenames as an argument to\n+        # get_dict_files\n+        files = self.get_filenames(self.directory)\n+        files_dict = self.get_files_dict(files)\n+        return files_dict\n+\n+\n+class Differ(object):\n+\n+    def __init__(self, former, latter):\n+        self.former = former\n+        self.latter = latter\n+        self._additions = False\n+        self._operation = self.diff_operation\n+\n+    @property\n+    def additions(self):\n+        return self._additions\n+\n+    @additions.setter\n+    def additions(self, value):\n+        # If measuring additions instead of deletions, automatically change operator.\n+        self._additions = value\n+        if value:\n+            self._operation = self.combinaton_diff_operation\n+        else:\n+            self._operation = self.diff_operation\n+\n+\n+    def diff_operation(self, a, b):\n+        # Use this function to find the difference between two sets of dictionary\n+        # keys. Uses the set difference operation. Called by the diff_modules\n+        # function.\n+        return list(a - b)\n+\n+    def combinaton_diff_operation(self, a, b):\n+        # Use a combination of symmetric_difference and difference to return a list\n+        # of items additions through the crawl. Allows you to get additions items instead of\n+        # removed items. Would produce the same result if you switched the\n+        # positions of the former and latter versions. Much more natural and\n+        # easy control using the different set operators.\n+        return list((a ^ b) - a)\n+\n+    def diff_files(self):\n+        diff = {}\n+        intersection = {}\n+\n+        intersection.update(self.latter)\n+        intersection.update(self.former)\n+        files_diff = self._operation(set(self.former), set(self.latter))\n+\n+        # Diff files\n+        for x in intersection.keys():\n+            if x in files_diff:\n+                diff[x] = {}\n+        return intersection, diff\n+\n+    def diff_functions_classes(self, diff, intersection):\n+        for x in intersection.keys():\n+            former_items = self.former.get(x)\n+            latter_items = self.latter.get(x)\n+            if former_items and latter_items:\n+                function_diff = self._operation(\n+                    set(former_items[\"functions\"]),\n+                    set(latter_items[\"functions\"])\n+                )\n+                class_diff = self._operation(\n+                    set(list(former_items[\"classes\"].keys())),\n+                    set(list(latter_items[\"classes\"].keys()))\n+                )\n+                if function_diff or list(class_diff):\n+                    diff[x]= copy.deepcopy(intersection[x])\n+                    if list(class_diff):\n+                        diff_dict = {y: {} for y in class_diff}\n+                        diff[x][\"classes\"] = diff_dict\n+                    else:\n+                        diff[x][\"classes\"] = {}\n+\n+                    if function_diff:\n+                        diff[x][\"functions\"] = list(function_diff)\n+                    else:\n+                        diff[x][\"functions\"] = []\n+        return diff\n+\n+    def diff_methods(self, diff, intersection):\n+        for x in intersection.keys():\n+            former_classes = self.former[x].get(\"classes\") if self.former.get(x) else {}\n+            latter_classes = self.latter[x].get(\"classes\") if self.latter.get(x) else {}\n+            if former_classes and latter_classes:\n+                for y in intersection[x][\"classes\"]:\n+                    # Prevent NoneType errors by returning empty dict.\n+                    former_methods = self.former[x][\"classes\"].get(y, {})\n+                    latter_methods = self.latter[x][\"classes\"].get(y, {})\n+                    if former_methods.get(\"methods\") and latter_methods.get(\"methods\"):\n+                        former_values = set(list(former_methods.values())[0])\n+                        latter_values = set(list(latter_methods.values())[0])\n+                        methods_diff = self._operation(former_values, latter_values)\n+                        if methods_diff:\n+                            if not diff.get(x):\n+                                diff[x] = {}\n+                                diff[x][\"classes\"] = {}\n+                            diff[x][\"classes\"][y] = copy.deepcopy(intersection[x][\"classes\"][y])\n+                            diff[x][\"classes\"][y][\"methods\"] = methods_diff\n+        return diff\n+\n+    def diff_modules(self):\n+        intersection, diff = self.diff_files()\n+        diff = self.diff_functions_classes(diff, intersection)\n+        diff = self.diff_methods(diff, intersection)\n+        return diff\n+\n+    def parse_diff(self, diff):\n+        parsed_diff = []\n+        if self.additions:\n+            method = \"ADDED\"\n+        else:\n+            method = \"DELETED\"\n+        for x in diff.keys():\n+            formatted_string = \"%s %s\" % (method, os.path.splitext(x.replace(\"/\", \".\"))[0])\n+            if diff[x].values():\n+                for y in diff[x].values():\n+                    if isinstance(y, dict) and y:\n+                        for z in y.keys():\n+                            if not y[z].values():\n+                                parsed_diff.append(\"%s.%s\" % (formatted_string, z))\n+                            else:\n+                                for a in y[z].values():\n+                                    for b in a:\n+                                        parsed_diff.append(\"%s.%s.%s\" % (formatted_string, z, b))\n+                    elif isinstance(y, list) and y:\n+                        for z in y:\n+                            class_string = \"%s\" % z\n+                            parsed_diff.append(\"%s.%s\" % (formatted_string, class_string))\n+            else:\n+                parsed_diff.insert(0, formatted_string)\n+        return parsed_diff\n+\n+    def get_diff(self):\n+        self.additions = False","path":"bokeh/util/api_crawler.py","position":224,"original_position":224,"commit_id":"2df330025ea2217f8aeb89698de49a57a826247e","original_commit_id":"2df330025ea2217f8aeb89698de49a57a826247e","user":{"login":"brittainhard","id":5440553,"avatar_url":"https://avatars.githubusercontent.com/u/5440553?v=3","gravatar_id":"","url":"https://api.github.com/users/brittainhard","html_url":"https://github.com/brittainhard","followers_url":"https://api.github.com/users/brittainhard/followers","following_url":"https://api.github.com/users/brittainhard/following{/other_user}","gists_url":"https://api.github.com/users/brittainhard/gists{/gist_id}","starred_url":"https://api.github.com/users/brittainhard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brittainhard/subscriptions","organizations_url":"https://api.github.com/users/brittainhard/orgs","repos_url":"https://api.github.com/users/brittainhard/repos","events_url":"https://api.github.com/users/brittainhard/events{/privacy}","received_events_url":"https://api.github.com/users/brittainhard/received_events","type":"User","site_admin":false},"body":"Do you have a particular opinion on how it should be done? I'm planning some refactoring when I include function signatures.","created_at":"2016-08-24T14:06:22Z","updated_at":"2016-08-24T14:06:22Z","html_url":"https://github.com/bokeh/bokeh/pull/4921#discussion_r76062460","pull_request_url":"https://api.github.com/repos/bokeh/bokeh/pulls/4921","_links":{"self":{"href":"https://api.github.com/repos/bokeh/bokeh/pulls/comments/76062460"},"html":{"href":"https://github.com/bokeh/bokeh/pull/4921#discussion_r76062460"},"pull_request":{"href":"https://api.github.com/repos/bokeh/bokeh/pulls/4921"}}},"pull_request":{"url":"https://api.github.com/repos/bokeh/bokeh/pulls/4921","id":79927409,"html_url":"https://github.com/bokeh/bokeh/pull/4921","diff_url":"https://github.com/bokeh/bokeh/pull/4921.diff","patch_url":"https://github.com/bokeh/bokeh/pull/4921.patch","issue_url":"https://api.github.com/repos/bokeh/bokeh/issues/4921","number":4921,"state":"open","locked":false,"title":"Module Crawl and Version Diff","user":{"login":"brittainhard","id":5440553,"avatar_url":"https://avatars.githubusercontent.com/u/5440553?v=3","gravatar_id":"","url":"https://api.github.com/users/brittainhard","html_url":"https://github.com/brittainhard","followers_url":"https://api.github.com/users/brittainhard/followers","following_url":"https://api.github.com/users/brittainhard/following{/other_user}","gists_url":"https://api.github.com/users/brittainhard/gists{/gist_id}","starred_url":"https://api.github.com/users/brittainhard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brittainhard/subscriptions","organizations_url":"https://api.github.com/users/brittainhard/orgs","repos_url":"https://api.github.com/users/brittainhard/repos","events_url":"https://api.github.com/users/brittainhard/events{/privacy}","received_events_url":"https://api.github.com/users/brittainhard/received_events","type":"User","site_admin":false},"body":" fixes #4891 fixes #4892 \r\n\r\n- [x] Script to crawl folders and grab classes and functions from each file.\r\n- [x] Script to diff between old and new versions","created_at":"2016-08-03T15:30:16Z","updated_at":"2016-08-24T14:06:22Z","closed_at":null,"merged_at":null,"merge_commit_sha":"136caf182434d92bb6aeee591b0fc275792ab7e4","assignee":{"login":"brittainhard","id":5440553,"avatar_url":"https://avatars.githubusercontent.com/u/5440553?v=3","gravatar_id":"","url":"https://api.github.com/users/brittainhard","html_url":"https://github.com/brittainhard","followers_url":"https://api.github.com/users/brittainhard/followers","following_url":"https://api.github.com/users/brittainhard/following{/other_user}","gists_url":"https://api.github.com/users/brittainhard/gists{/gist_id}","starred_url":"https://api.github.com/users/brittainhard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brittainhard/subscriptions","organizations_url":"https://api.github.com/users/brittainhard/orgs","repos_url":"https://api.github.com/users/brittainhard/repos","events_url":"https://api.github.com/users/brittainhard/events{/privacy}","received_events_url":"https://api.github.com/users/brittainhard/received_events","type":"User","site_admin":false},"assignees":[{"login":"brittainhard","id":5440553,"avatar_url":"https://avatars.githubusercontent.com/u/5440553?v=3","gravatar_id":"","url":"https://api.github.com/users/brittainhard","html_url":"https://github.com/brittainhard","followers_url":"https://api.github.com/users/brittainhard/followers","following_url":"https://api.github.com/users/brittainhard/following{/other_user}","gists_url":"https://api.github.com/users/brittainhard/gists{/gist_id}","starred_url":"https://api.github.com/users/brittainhard/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brittainhard/subscriptions","organizations_url":"https://api.github.com/users/brittainhard/orgs","repos_url":"https://api.github.com/users/brittainhard/repos","events_url":"https://api.github.com/users/brittainhard/events{/privacy}","received_events_url":"https://api.github.com/users/brittainhard/received_events","type":"User","site_admin":false}],"milestone":null,"commits_url":"https://api.github.com/repos/bokeh/bokeh/pulls/4921/commits","review_comments_url":"https://api.github.com/repos/bokeh/bokeh/pulls/4921/comments","review_comment_url":"https://api.github.com/repos/bokeh/bokeh/pulls/comments{/number}","comments_url":"https://api.github.com/repos/bokeh/bokeh/issues/4921/comments","statuses_url":"https://api.github.com/repos/bokeh/bokeh/statuses/2df330025ea2217f8aeb89698de49a57a826247e","head":{"label":"bokeh:bhard/parse_modules","ref":"bhard/parse_modules","sha":"2df330025ea2217f8aeb89698de49a57a826247e","user":{"login":"bokeh","id":8440965,"avatar_url":"https://avatars.githubusercontent.com/u/8440965?v=3","gravatar_id":"","url":"https://api.github.com/users/bokeh","html_url":"https://github.com/bokeh","followers_url":"https://api.github.com/users/bokeh/followers","following_url":"https://api.github.com/users/bokeh/following{/other_user}","gists_url":"https://api.github.com/users/bokeh/gists{/gist_id}","starred_url":"https://api.github.com/users/bokeh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bokeh/subscriptions","organizations_url":"https://api.github.com/users/bokeh/orgs","repos_url":"https://api.github.com/users/bokeh/repos","events_url":"https://api.github.com/users/bokeh/events{/privacy}","received_events_url":"https://api.github.com/users/bokeh/received_events","type":"Organization","site_admin":false},"repo":{"id":3834332,"name":"bokeh","full_name":"bokeh/bokeh","owner":{"login":"bokeh","id":8440965,"avatar_url":"https://avatars.githubusercontent.com/u/8440965?v=3","gravatar_id":"","url":"https://api.github.com/users/bokeh","html_url":"https://github.com/bokeh","followers_url":"https://api.github.com/users/bokeh/followers","following_url":"https://api.github.com/users/bokeh/following{/other_user}","gists_url":"https://api.github.com/users/bokeh/gists{/gist_id}","starred_url":"https://api.github.com/users/bokeh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bokeh/subscriptions","organizations_url":"https://api.github.com/users/bokeh/orgs","repos_url":"https://api.github.com/users/bokeh/repos","events_url":"https://api.github.com/users/bokeh/events{/privacy}","received_events_url":"https://api.github.com/users/bokeh/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/bokeh/bokeh","description":"Interactive Web Plotting for Python","fork":false,"url":"https://api.github.com/repos/bokeh/bokeh","forks_url":"https://api.github.com/repos/bokeh/bokeh/forks","keys_url":"https://api.github.com/repos/bokeh/bokeh/keys{/key_id}","collaborators_url":"https://api.github.com/repos/bokeh/bokeh/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/bokeh/bokeh/teams","hooks_url":"https://api.github.com/repos/bokeh/bokeh/hooks","issue_events_url":"https://api.github.com/repos/bokeh/bokeh/issues/events{/number}","events_url":"https://api.github.com/repos/bokeh/bokeh/events","assignees_url":"https://api.github.com/repos/bokeh/bokeh/assignees{/user}","branches_url":"https://api.github.com/repos/bokeh/bokeh/branches{/branch}","tags_url":"https://api.github.com/repos/bokeh/bokeh/tags","blobs_url":"https://api.github.com/repos/bokeh/bokeh/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/bokeh/bokeh/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/bokeh/bokeh/git/refs{/sha}","trees_url":"https://api.github.com/repos/bokeh/bokeh/git/trees{/sha}","statuses_url":"https://api.github.com/repos/bokeh/bokeh/statuses/{sha}","languages_url":"https://api.github.com/repos/bokeh/bokeh/languages","stargazers_url":"https://api.github.com/repos/bokeh/bokeh/stargazers","contributors_url":"https://api.github.com/repos/bokeh/bokeh/contributors","subscribers_url":"https://api.github.com/repos/bokeh/bokeh/subscribers","subscription_url":"https://api.github.com/repos/bokeh/bokeh/subscription","commits_url":"https://api.github.com/repos/bokeh/bokeh/commits{/sha}","git_commits_url":"https://api.github.com/repos/bokeh/bokeh/git/commits{/sha}","comments_url":"https://api.github.com/repos/bokeh/bokeh/comments{/number}","issue_comment_url":"https://api.github.com/repos/bokeh/bokeh/issues/comments{/number}","contents_url":"https://api.github.com/repos/bokeh/bokeh/contents/{+path}","compare_url":"https://api.github.com/repos/bokeh/bokeh/compare/{base}...{head}","merges_url":"https://api.github.com/repos/bokeh/bokeh/merges","archive_url":"https://api.github.com/repos/bokeh/bokeh/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/bokeh/bokeh/downloads","issues_url":"https://api.github.com/repos/bokeh/bokeh/issues{/number}","pulls_url":"https://api.github.com/repos/bokeh/bokeh/pulls{/number}","milestones_url":"https://api.github.com/repos/bokeh/bokeh/milestones{/number}","notifications_url":"https://api.github.com/repos/bokeh/bokeh/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/bokeh/bokeh/labels{/name}","releases_url":"https://api.github.com/repos/bokeh/bokeh/releases{/id}","deployments_url":"https://api.github.com/repos/bokeh/bokeh/deployments","created_at":"2012-03-26T15:40:01Z","updated_at":"2016-08-23T17:39:39Z","pushed_at":"2016-08-24T02:21:10Z","git_url":"git://github.com/bokeh/bokeh.git","ssh_url":"git@github.com:bokeh/bokeh.git","clone_url":"https://github.com/bokeh/bokeh.git","svn_url":"https://github.com/bokeh/bokeh","homepage":"http://bokeh.pydata.org/en/latest/","size":184557,"stargazers_count":4555,"watchers_count":4555,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1023,"mirror_url":null,"open_issues_count":722,"forks":1023,"open_issues":722,"watchers":4555,"default_branch":"master"}},"base":{"label":"bokeh:master","ref":"master","sha":"fba4673e2ea22422066e2826833cd5336bd62688","user":{"login":"bokeh","id":8440965,"avatar_url":"https://avatars.githubusercontent.com/u/8440965?v=3","gravatar_id":"","url":"https://api.github.com/users/bokeh","html_url":"https://github.com/bokeh","followers_url":"https://api.github.com/users/bokeh/followers","following_url":"https://api.github.com/users/bokeh/following{/other_user}","gists_url":"https://api.github.com/users/bokeh/gists{/gist_id}","starred_url":"https://api.github.com/users/bokeh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bokeh/subscriptions","organizations_url":"https://api.github.com/users/bokeh/orgs","repos_url":"https://api.github.com/users/bokeh/repos","events_url":"https://api.github.com/users/bokeh/events{/privacy}","received_events_url":"https://api.github.com/users/bokeh/received_events","type":"Organization","site_admin":false},"repo":{"id":3834332,"name":"bokeh","full_name":"bokeh/bokeh","owner":{"login":"bokeh","id":8440965,"avatar_url":"https://avatars.githubusercontent.com/u/8440965?v=3","gravatar_id":"","url":"https://api.github.com/users/bokeh","html_url":"https://github.com/bokeh","followers_url":"https://api.github.com/users/bokeh/followers","following_url":"https://api.github.com/users/bokeh/following{/other_user}","gists_url":"https://api.github.com/users/bokeh/gists{/gist_id}","starred_url":"https://api.github.com/users/bokeh/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bokeh/subscriptions","organizations_url":"https://api.github.com/users/bokeh/orgs","repos_url":"https://api.github.com/users/bokeh/repos","events_url":"https://api.github.com/users/bokeh/events{/privacy}","received_events_url":"https://api.github.com/users/bokeh/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/bokeh/bokeh","description":"Interactive Web Plotting for Python","fork":false,"url":"https://api.github.com/repos/bokeh/bokeh","forks_url":"https://api.github.com/repos/bokeh/bokeh/forks","keys_url":"https://api.github.com/repos/bokeh/bokeh/keys{/key_id}","collaborators_url":"https://api.github.com/repos/bokeh/bokeh/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/bokeh/bokeh/teams","hooks_url":"https://api.github.com/repos/bokeh/bokeh/hooks","issue_events_url":"https://api.github.com/repos/bokeh/bokeh/issues/events{/number}","events_url":"https://api.github.com/repos/bokeh/bokeh/events","assignees_url":"https://api.github.com/repos/bokeh/bokeh/assignees{/user}","branches_url":"https://api.github.com/repos/bokeh/bokeh/branches{/branch}","tags_url":"https://api.github.com/repos/bokeh/bokeh/tags","blobs_url":"https://api.github.com/repos/bokeh/bokeh/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/bokeh/bokeh/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/bokeh/bokeh/git/refs{/sha}","trees_url":"https://api.github.com/repos/bokeh/bokeh/git/trees{/sha}","statuses_url":"https://api.github.com/repos/bokeh/bokeh/statuses/{sha}","languages_url":"https://api.github.com/repos/bokeh/bokeh/languages","stargazers_url":"https://api.github.com/repos/bokeh/bokeh/stargazers","contributors_url":"https://api.github.com/repos/bokeh/bokeh/contributors","subscribers_url":"https://api.github.com/repos/bokeh/bokeh/subscribers","subscription_url":"https://api.github.com/repos/bokeh/bokeh/subscription","commits_url":"https://api.github.com/repos/bokeh/bokeh/commits{/sha}","git_commits_url":"https://api.github.com/repos/bokeh/bokeh/git/commits{/sha}","comments_url":"https://api.github.com/repos/bokeh/bokeh/comments{/number}","issue_comment_url":"https://api.github.com/repos/bokeh/bokeh/issues/comments{/number}","contents_url":"https://api.github.com/repos/bokeh/bokeh/contents/{+path}","compare_url":"https://api.github.com/repos/bokeh/bokeh/compare/{base}...{head}","merges_url":"https://api.github.com/repos/bokeh/bokeh/merges","archive_url":"https://api.github.com/repos/bokeh/bokeh/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/bokeh/bokeh/downloads","issues_url":"https://api.github.com/repos/bokeh/bokeh/issues{/number}","pulls_url":"https://api.github.com/repos/bokeh/bokeh/pulls{/number}","milestones_url":"https://api.github.com/repos/bokeh/bokeh/milestones{/number}","notifications_url":"https://api.github.com/repos/bokeh/bokeh/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/bokeh/bokeh/labels{/name}","releases_url":"https://api.github.com/repos/bokeh/bokeh/releases{/id}","deployments_url":"https://api.github.com/repos/bokeh/bokeh/deployments","created_at":"2012-03-26T15:40:01Z","updated_at":"2016-08-23T17:39:39Z","pushed_at":"2016-08-24T02:21:10Z","git_url":"git://github.com/bokeh/bokeh.git","ssh_url":"git@github.com:bokeh/bokeh.git","clone_url":"https://github.com/bokeh/bokeh.git","svn_url":"https://github.com/bokeh/bokeh","homepage":"http://bokeh.pydata.org/en/latest/","size":184557,"stargazers_count":4555,"watchers_count":4555,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":1023,"mirror_url":null,"open_issues_count":722,"forks":1023,"open_issues":722,"watchers":4555,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/bokeh/bokeh/pulls/4921"},"html":{"href":"https://github.com/bokeh/bokeh/pull/4921"},"issue":{"href":"https://api.github.com/repos/bokeh/bokeh/issues/4921"},"comments":{"href":"https://api.github.com/repos/bokeh/bokeh/issues/4921/comments"},"review_comments":{"href":"https://api.github.com/repos/bokeh/bokeh/pulls/4921/comments"},"review_comment":{"href":"https://api.github.com/repos/bokeh/bokeh/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/bokeh/bokeh/pulls/4921/commits"},"statuses":{"href":"https://api.github.com/repos/bokeh/bokeh/statuses/2df330025ea2217f8aeb89698de49a57a826247e"}}}}
{"actor":{"display_login":"brittainhard"}}