{"action":"created","comment":{"url":"https://api.github.com/repos/OCA/server-tools/pulls/comments/90284828","pull_request_review_id":10805986,"id":90284828,"diff_hunk":"@@ -0,0 +1,316 @@\n+# -*- coding: utf-8 -*-\n+# Copyright 2016 SYLEAM\n+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).\n+\n+import hashlib\n+import json\n+import logging\n+import werkzeug.utils\n+import werkzeug.wrappers\n+from datetime import datetime\n+from openerp import http, fields\n+\n+_logger = logging.getLogger(__name__)\n+\n+try:\n+    import oauthlib\n+    from oauthlib import oauth2\n+except ImportError:\n+    _logger.debug('Cannot `import oauthlib`.')\n+\n+\n+class OAuth2ProviderController(http.Controller):\n+    def __init__(self):\n+        super(OAuth2ProviderController, self).__init__()\n+\n+    @http.route('/oauth2/authorize', type='http', auth='user', methods=['GET'])\n+    def authorize(self, client_id=None, response_type=None, redirect_uri=None,\n+                  scope=None, state=None, *args, **kwargs):\n+        \"\"\"\n+        Check client's request, and display an authorization page to the user,\n+        listing allowed scopes\n+        If the client is configured to skip the authorization page, directly\n+        redirects to the requested URI\n+        \"\"\"\n+        client = http.request.env['oauth.provider.client'].search([\n+            ('identifier', '=', client_id),\n+        ])\n+        if not client:\n+            return http.request.render(\n+                'oauth_provider.authorization_error', {\n+                    'title': 'Unknown Client Identifier!',\n+                    'message': 'This client identifier is invalid.',\n+                })\n+        oauth2_server = client.get_oauth2_server()\n+\n+        # Retrieve needed arguments for oauthlib methods\n+        uri = http.request.httprequest.base_url\n+        http_method = http.request.httprequest.method\n+        body = oauthlib.common.urlencode(\n+            http.request.httprequest.values.items())\n+        headers = http.request.httprequest.headers\n+        try:\n+            scopes, credentials = oauth2_server.validate_authorization_request(\n+                uri, http_method=http_method, body=body, headers=headers)\n+            # Store only some values, because the pickling of the full request\n+            # object is not possible\n+            http.request.httpsession['oauth_scopes'] = scopes\n+            http.request.httpsession['oauth_credentials'] = {\n+                'client_id': credentials['client_id'],\n+                'redirect_uri': credentials['redirect_uri'],\n+                'response_type': credentials['response_type'],\n+                'state': credentials['state'],\n+            }\n+            if client.skip_authorization:\n+                # Skip the authorization page\n+                # Useful when the application is trusted\n+                headers, body, status = oauth2_server.\\\n+                    create_authorization_response(\n+                        uri, http_method=http_method, body=body,\n+                        headers=headers, scopes=scopes,\n+                        credentials=credentials)\n+                return werkzeug.utils.redirect(\n+                    headers['Location'], code=status)\n+        except oauth2.FatalClientError as e:\n+            return http.request.render(\n+                'oauth_provider.authorization_error', {\n+                    'title': 'Error: {error}'.format(error=e.error),\n+                    'message': e.description,\n+                })\n+        except oauth2.OAuth2Error as e:\n+            return http.request.render(\n+                'oauth_provider.authorization_error', {\n+                    'title': 'Error: {error}'.format(error=e.error),\n+                    'message': 'An unknown error occured! Please contact your '\n+                    'administrator',\n+                })\n+\n+        oauth_scopes = client.scope_ids.filtered(\n+            lambda record: record.code in scopes)\n+        return http.request.render(\n+            'oauth_provider.authorization', {\n+                'oauth_client': client.name,\n+                'oauth_scopes': oauth_scopes,\n+            })\n+\n+    @http.route(\n+        '/oauth2/authorize', type='http', auth='user', methods=['POST'])\n+    def authorize_post(self, *args, **kwargs):\n+        \"\"\"\n+        Redirect to the requested URI during the authorization\n+        \"\"\"\n+        client = http.request.env['oauth.provider.client'].search([\n+            ('identifier', '=', http.request.httpsession.get(\n+                'oauth_credentials', {}).get('client_id'))])\n+        if not client:\n+            return http.request.render(\n+                'oauth_provider.authorization_error', {\n+                    'title': 'Unknown Client Identifier!',\n+                    'message': 'This client identifier is invalid.',\n+                })\n+        oauth2_server = client.get_oauth2_server()\n+\n+        # Retrieve needed arguments for oauthlib methods\n+        uri = http.request.httprequest.base_url\n+        http_method = http.request.httprequest.method\n+        body = oauthlib.common.urlencode(\n+            http.request.httprequest.values.items())\n+        headers = http.request.httprequest.headers\n+        scopes = http.request.httpsession['oauth_scopes']\n+        credentials = http.request.httpsession['oauth_credentials']\n+        headers, body, status = oauth2_server.create_authorization_response(\n+            uri, http_method=http_method, body=body, headers=headers,\n+            scopes=scopes, credentials=credentials)\n+\n+        return werkzeug.utils.redirect(headers['Location'], code=status)\n+\n+    @http.route('/oauth2/token', type='http', auth='none', methods=['GET'])\n+    def token(self, client_id=None, client_secret=None, redirect_uri=None,\n+              scope=None, code=None, grant_type=None, username=None,\n+              password=None, refresh_token=None, *args, **kwargs):\n+        \"\"\"\n+        Return a token corresponding to the supplied information\n+        Not all parameters are required, depenging on the application type\n+        \"\"\"\n+        client = http.request.env['oauth.provider.client'].search([\n+            ('identifier', '=', http.request.httpsession.get(\n+                'oauth_credentials', {}).get('client_id'))])\n+\n+        if not client:\n+            return json.dumps({'error': 'invalid_client_id'})\n+        oauth2_server = client.get_oauth2_server()\n+\n+        # Retrieve needed arguments for oauthlib methods\n+        uri = http.request.httprequest.base_url\n+        http_method = http.request.httprequest.method\n+        body = oauthlib.common.urlencode(\n+            http.request.httprequest.values.items())\n+        headers = http.request.httprequest.headers\n+        credentials = {'scope': scope}\n+\n+        # Retrieve the authorization code, if any, to get Odoo's user id\n+        existing_code = http.request.env[\n+            'oauth.provider.authorization.code'].search([\n+                ('client_id.identifier', '=', http.request.httpsession.get(\n+                    'oauth_credentials', {}).get('client_id')),\n+                ('code', '=', code),\n+            ])\n+        if existing_code:\n+            credentials['odoo_user_id'] = existing_code.user_id.id\n+        # Retrieve the existing token, if any, to get Odoo's user id\n+        existing_token = http.request.env['oauth.provider.token'].search([\n+            ('client_id.identifier', '=', http.request.httpsession.get(\n+                'oauth_credentials', {}).get('client_id')),\n+            ('refresh_token', '=', refresh_token),\n+        ])\n+        if existing_token:\n+            credentials['odoo_user_id'] = existing_token.user_id.id\n+\n+        headers, body, status = oauth2_server.create_token_response(\n+            uri, http_method=http_method, body=body, headers=headers,\n+            credentials=credentials)\n+\n+        return werkzeug.wrappers.BaseResponse(\n+            body, status=status, headers=headers)\n+\n+    @http.route('/oauth2/tokeninfo', type='http', auth='none', methods=['GET'])\n+    def tokeninfo(self, access_token=None, *args, **kwargs):\n+        \"\"\"\n+        Return some information about the supplied token\n+        Similar to Google's \"tokeninfo\" request\n+        \"\"\"\n+        token = http.request.env['oauth.provider.token'].search([\n+            ('token', '=', access_token),\n+        ])\n+        if not token:\n+            return json.dumps({'error': 'invalid_or_expired_token'})\n+        oauth2_server = token.client_id.get_oauth2_server()\n+\n+        # Retrieve needed arguments for oauthlib methods\n+        uri = http.request.httprequest.base_url\n+        http_method = http.request.httprequest.method\n+        body = oauthlib.common.urlencode(\n+            http.request.httprequest.values.items())\n+        headers = http.request.httprequest.headers\n+\n+        valid, oauthlib_request = oauth2_server.verify_request(\n+            uri, http_method=http_method, body=body, headers=headers)\n+        if not valid:\n+            return json.dumps({})\n+\n+        token_lifetime = (fields.Datetime.from_string(token.expires_at) -\n+                          datetime.now()).seconds\n+        # Base data to return\n+        data = {\n+            'audience': token.client_id.identifier,\n+            'scopes': ' '.join(token.scope_ids.mapped('code')),\n+            'expires_in': token_lifetime,\n+        }\n+\n+        # Add the oauth user identifier, if user's information access is\n+        # allowed by the token's scopes\n+        user_data = token.get_data_for_model(\n+            'res.users', res_id=token.user_id.id)\n+        if 'id' in user_data:\n+            # Include the client and user identifiers in the final identifier\n+            # to generate a different identifier for the same user, depending\n+            # on the client accessing this user\n+            # By doing this, clients cannot find a list of common users by\n+            # comparing their identifiers list from tokeninfo\n+            user_identifier = token.client_id.identifier\n+            user_identifier += token.user_id.oauth_identifier\n+            # Use a sha256 to avoid a too long final string\n+            data.update(user_id=hashlib.sha256(user_identifier).hexdigest())\n+        return json.dumps(data)\n+\n+    @http.route('/oauth2/userinfo', type='http', auth='none', methods=['GET'])\n+    def userinfo(self, access_token=None, *args, **kwargs):\n+        \"\"\"\n+        Return some information about the user linked to the supplied token\n+        Similar to Google's \"userinfo\" request\n+        \"\"\"\n+        token = http.request.env['oauth.provider.token'].search([\n+            ('token', '=', access_token),\n+        ])\n+        if not token:\n+            return json.dumps({'error': 'invalid_or_expired_token'})\n+        oauth2_server = token.client_id.get_oauth2_server()\n+\n+        # Retrieve needed arguments for oauthlib methods\n+        uri = http.request.httprequest.base_url\n+        http_method = http.request.httprequest.method\n+        body = oauthlib.common.urlencode(\n+            http.request.httprequest.values.items())\n+        headers = http.request.httprequest.headers\n+\n+        valid, oauthlib_request = oauth2_server.verify_request(","path":"oauth_provider/controllers/main.py","position":196,"original_position":246,"commit_id":"a5dc3af03dc984198cc884b69212557a6015d194","original_commit_id":"a5dc3af03dc984198cc884b69212557a6015d194","user":{"login":"lasley","id":7397578,"avatar_url":"https://avatars.githubusercontent.com/u/7397578?v=3","gravatar_id":"","url":"https://api.github.com/users/lasley","html_url":"https://github.com/lasley","followers_url":"https://api.github.com/users/lasley/followers","following_url":"https://api.github.com/users/lasley/following{/other_user}","gists_url":"https://api.github.com/users/lasley/gists{/gist_id}","starred_url":"https://api.github.com/users/lasley/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lasley/subscriptions","organizations_url":"https://api.github.com/users/lasley/orgs","repos_url":"https://api.github.com/users/lasley/repos","events_url":"https://api.github.com/users/lasley/events{/privacy}","received_events_url":"https://api.github.com/users/lasley/received_events","type":"User","site_admin":false},"body":"This token & argument retrieval followed verification block seems to be repeated a lot. It would be nice if it was just a helper method instead.","created_at":"2016-11-30T17:34:28Z","updated_at":"2016-11-30T17:35:12Z","html_url":"https://github.com/OCA/server-tools/pull/611#discussion_r90284828","pull_request_url":"https://api.github.com/repos/OCA/server-tools/pulls/611","_links":{"self":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/comments/90284828"},"html":{"href":"https://github.com/OCA/server-tools/pull/611#discussion_r90284828"},"pull_request":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/611"}}},"pull_request":{"url":"https://api.github.com/repos/OCA/server-tools/pulls/611","id":94006629,"html_url":"https://github.com/OCA/server-tools/pull/611","diff_url":"https://github.com/OCA/server-tools/pull/611.diff","patch_url":"https://github.com/OCA/server-tools/pull/611.patch","issue_url":"https://api.github.com/repos/OCA/server-tools/issues/611","number":611,"state":"open","locked":false,"title":"[9.0] Add oauth_provider module","user":{"login":"sylvain-garancher","id":2804014,"avatar_url":"https://avatars.githubusercontent.com/u/2804014?v=3","gravatar_id":"","url":"https://api.github.com/users/sylvain-garancher","html_url":"https://github.com/sylvain-garancher","followers_url":"https://api.github.com/users/sylvain-garancher/followers","following_url":"https://api.github.com/users/sylvain-garancher/following{/other_user}","gists_url":"https://api.github.com/users/sylvain-garancher/gists{/gist_id}","starred_url":"https://api.github.com/users/sylvain-garancher/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sylvain-garancher/subscriptions","organizations_url":"https://api.github.com/users/sylvain-garancher/orgs","repos_url":"https://api.github.com/users/sylvain-garancher/repos","events_url":"https://api.github.com/users/sylvain-garancher/events{/privacy}","received_events_url":"https://api.github.com/users/sylvain-garancher/received_events","type":"User","site_admin":false},"body":"A new module that allows to use Odoo as OAuth provider.\r\nThe OAuth protocol is used to delegate authentication, and authorization to access some resources.\r\n\r\nThis module really need security reviews/tests, as I'm not fully comfortable with the OAuh protocol.\r\n\r\nThe RFC defines four profiles: Implicit grant, Authorization code grant, Password credentials grand and User credentials grant.\r\nOnly Implicit grant and Authorization code grant are implemented yet, and I don't plan to implement the others for now. The work should not be very hard, as most parts are common, but I don't actually need these profiles, and don't have time to do it for the moment.\r\n\r\nNote: I created the PR to allow you to begin testing the module.\r\n@pedrobaeza said that this repo will be split into several smaller repos, this PR will need to be recreated on one of the new repos when they are created.","created_at":"2016-11-16T16:58:38Z","updated_at":"2016-11-30T17:56:12Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0520b044ae3f7fcc79b4fd4f95c6b17e9826cc4d","assignee":null,"assignees":[],"milestone":{"url":"https://api.github.com/repos/OCA/server-tools/milestones/4","html_url":"https://github.com/OCA/server-tools/milestones/9.0","labels_url":"https://api.github.com/repos/OCA/server-tools/milestones/4/labels","id":1354400,"number":4,"title":"9.0","description":null,"creator":{"login":"pedrobaeza","id":7165771,"avatar_url":"https://avatars.githubusercontent.com/u/7165771?v=3","gravatar_id":"","url":"https://api.github.com/users/pedrobaeza","html_url":"https://github.com/pedrobaeza","followers_url":"https://api.github.com/users/pedrobaeza/followers","following_url":"https://api.github.com/users/pedrobaeza/following{/other_user}","gists_url":"https://api.github.com/users/pedrobaeza/gists{/gist_id}","starred_url":"https://api.github.com/users/pedrobaeza/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pedrobaeza/subscriptions","organizations_url":"https://api.github.com/users/pedrobaeza/orgs","repos_url":"https://api.github.com/users/pedrobaeza/repos","events_url":"https://api.github.com/users/pedrobaeza/events{/privacy}","received_events_url":"https://api.github.com/users/pedrobaeza/received_events","type":"User","site_admin":false},"open_issues":8,"closed_issues":47,"state":"open","created_at":"2015-10-14T00:54:02Z","updated_at":"2016-11-30T17:21:16Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/OCA/server-tools/pulls/611/commits","review_comments_url":"https://api.github.com/repos/OCA/server-tools/pulls/611/comments","review_comment_url":"https://api.github.com/repos/OCA/server-tools/pulls/comments{/number}","comments_url":"https://api.github.com/repos/OCA/server-tools/issues/611/comments","statuses_url":"https://api.github.com/repos/OCA/server-tools/statuses/a5dc3af03dc984198cc884b69212557a6015d194","head":{"label":"syleam:9.0-add-oauth_provider-module","ref":"9.0-add-oauth_provider-module","sha":"a5dc3af03dc984198cc884b69212557a6015d194","user":{"login":"syleam","id":2770992,"avatar_url":"https://avatars.githubusercontent.com/u/2770992?v=3","gravatar_id":"","url":"https://api.github.com/users/syleam","html_url":"https://github.com/syleam","followers_url":"https://api.github.com/users/syleam/followers","following_url":"https://api.github.com/users/syleam/following{/other_user}","gists_url":"https://api.github.com/users/syleam/gists{/gist_id}","starred_url":"https://api.github.com/users/syleam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/syleam/subscriptions","organizations_url":"https://api.github.com/users/syleam/orgs","repos_url":"https://api.github.com/users/syleam/repos","events_url":"https://api.github.com/users/syleam/events{/privacy}","received_events_url":"https://api.github.com/users/syleam/received_events","type":"Organization","site_admin":false},"repo":{"id":73913528,"name":"server-tools","full_name":"syleam/server-tools","owner":{"login":"syleam","id":2770992,"avatar_url":"https://avatars.githubusercontent.com/u/2770992?v=3","gravatar_id":"","url":"https://api.github.com/users/syleam","html_url":"https://github.com/syleam","followers_url":"https://api.github.com/users/syleam/followers","following_url":"https://api.github.com/users/syleam/following{/other_user}","gists_url":"https://api.github.com/users/syleam/gists{/gist_id}","starred_url":"https://api.github.com/users/syleam/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/syleam/subscriptions","organizations_url":"https://api.github.com/users/syleam/orgs","repos_url":"https://api.github.com/users/syleam/repos","events_url":"https://api.github.com/users/syleam/events{/privacy}","received_events_url":"https://api.github.com/users/syleam/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/syleam/server-tools","description":"Tools for Odoo Administrators to improve some technical features on Odoo.","fork":true,"url":"https://api.github.com/repos/syleam/server-tools","forks_url":"https://api.github.com/repos/syleam/server-tools/forks","keys_url":"https://api.github.com/repos/syleam/server-tools/keys{/key_id}","collaborators_url":"https://api.github.com/repos/syleam/server-tools/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/syleam/server-tools/teams","hooks_url":"https://api.github.com/repos/syleam/server-tools/hooks","issue_events_url":"https://api.github.com/repos/syleam/server-tools/issues/events{/number}","events_url":"https://api.github.com/repos/syleam/server-tools/events","assignees_url":"https://api.github.com/repos/syleam/server-tools/assignees{/user}","branches_url":"https://api.github.com/repos/syleam/server-tools/branches{/branch}","tags_url":"https://api.github.com/repos/syleam/server-tools/tags","blobs_url":"https://api.github.com/repos/syleam/server-tools/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/syleam/server-tools/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/syleam/server-tools/git/refs{/sha}","trees_url":"https://api.github.com/repos/syleam/server-tools/git/trees{/sha}","statuses_url":"https://api.github.com/repos/syleam/server-tools/statuses/{sha}","languages_url":"https://api.github.com/repos/syleam/server-tools/languages","stargazers_url":"https://api.github.com/repos/syleam/server-tools/stargazers","contributors_url":"https://api.github.com/repos/syleam/server-tools/contributors","subscribers_url":"https://api.github.com/repos/syleam/server-tools/subscribers","subscription_url":"https://api.github.com/repos/syleam/server-tools/subscription","commits_url":"https://api.github.com/repos/syleam/server-tools/commits{/sha}","git_commits_url":"https://api.github.com/repos/syleam/server-tools/git/commits{/sha}","comments_url":"https://api.github.com/repos/syleam/server-tools/comments{/number}","issue_comment_url":"https://api.github.com/repos/syleam/server-tools/issues/comments{/number}","contents_url":"https://api.github.com/repos/syleam/server-tools/contents/{+path}","compare_url":"https://api.github.com/repos/syleam/server-tools/compare/{base}...{head}","merges_url":"https://api.github.com/repos/syleam/server-tools/merges","archive_url":"https://api.github.com/repos/syleam/server-tools/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/syleam/server-tools/downloads","issues_url":"https://api.github.com/repos/syleam/server-tools/issues{/number}","pulls_url":"https://api.github.com/repos/syleam/server-tools/pulls{/number}","milestones_url":"https://api.github.com/repos/syleam/server-tools/milestones{/number}","notifications_url":"https://api.github.com/repos/syleam/server-tools/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/syleam/server-tools/labels{/name}","releases_url":"https://api.github.com/repos/syleam/server-tools/releases{/id}","deployments_url":"https://api.github.com/repos/syleam/server-tools/deployments","created_at":"2016-11-16T11:27:11Z","updated_at":"2016-11-16T11:27:14Z","pushed_at":"2016-11-28T10:52:22Z","git_url":"git://github.com/syleam/server-tools.git","ssh_url":"git@github.com:syleam/server-tools.git","clone_url":"https://github.com/syleam/server-tools.git","svn_url":"https://github.com/syleam/server-tools","homepage":"https://odoo-community.org/project/tools-maintainers-30","size":7328,"stargazers_count":0,"watchers_count":0,"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":0,"default_branch":"10.0"}},"base":{"label":"OCA:9.0","ref":"9.0","sha":"c71a77e8c1a187a55c5b64867222a1e2bf174885","user":{"login":"OCA","id":7600578,"avatar_url":"https://avatars.githubusercontent.com/u/7600578?v=3","gravatar_id":"","url":"https://api.github.com/users/OCA","html_url":"https://github.com/OCA","followers_url":"https://api.github.com/users/OCA/followers","following_url":"https://api.github.com/users/OCA/following{/other_user}","gists_url":"https://api.github.com/users/OCA/gists{/gist_id}","starred_url":"https://api.github.com/users/OCA/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OCA/subscriptions","organizations_url":"https://api.github.com/users/OCA/orgs","repos_url":"https://api.github.com/users/OCA/repos","events_url":"https://api.github.com/users/OCA/events{/privacy}","received_events_url":"https://api.github.com/users/OCA/received_events","type":"Organization","site_admin":false},"repo":{"id":20896532,"name":"server-tools","full_name":"OCA/server-tools","owner":{"login":"OCA","id":7600578,"avatar_url":"https://avatars.githubusercontent.com/u/7600578?v=3","gravatar_id":"","url":"https://api.github.com/users/OCA","html_url":"https://github.com/OCA","followers_url":"https://api.github.com/users/OCA/followers","following_url":"https://api.github.com/users/OCA/following{/other_user}","gists_url":"https://api.github.com/users/OCA/gists{/gist_id}","starred_url":"https://api.github.com/users/OCA/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/OCA/subscriptions","organizations_url":"https://api.github.com/users/OCA/orgs","repos_url":"https://api.github.com/users/OCA/repos","events_url":"https://api.github.com/users/OCA/events{/privacy}","received_events_url":"https://api.github.com/users/OCA/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/OCA/server-tools","description":"Tools for Odoo Administrators to improve some technical features on Odoo.","fork":false,"url":"https://api.github.com/repos/OCA/server-tools","forks_url":"https://api.github.com/repos/OCA/server-tools/forks","keys_url":"https://api.github.com/repos/OCA/server-tools/keys{/key_id}","collaborators_url":"https://api.github.com/repos/OCA/server-tools/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/OCA/server-tools/teams","hooks_url":"https://api.github.com/repos/OCA/server-tools/hooks","issue_events_url":"https://api.github.com/repos/OCA/server-tools/issues/events{/number}","events_url":"https://api.github.com/repos/OCA/server-tools/events","assignees_url":"https://api.github.com/repos/OCA/server-tools/assignees{/user}","branches_url":"https://api.github.com/repos/OCA/server-tools/branches{/branch}","tags_url":"https://api.github.com/repos/OCA/server-tools/tags","blobs_url":"https://api.github.com/repos/OCA/server-tools/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/OCA/server-tools/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/OCA/server-tools/git/refs{/sha}","trees_url":"https://api.github.com/repos/OCA/server-tools/git/trees{/sha}","statuses_url":"https://api.github.com/repos/OCA/server-tools/statuses/{sha}","languages_url":"https://api.github.com/repos/OCA/server-tools/languages","stargazers_url":"https://api.github.com/repos/OCA/server-tools/stargazers","contributors_url":"https://api.github.com/repos/OCA/server-tools/contributors","subscribers_url":"https://api.github.com/repos/OCA/server-tools/subscribers","subscription_url":"https://api.github.com/repos/OCA/server-tools/subscription","commits_url":"https://api.github.com/repos/OCA/server-tools/commits{/sha}","git_commits_url":"https://api.github.com/repos/OCA/server-tools/git/commits{/sha}","comments_url":"https://api.github.com/repos/OCA/server-tools/comments{/number}","issue_comment_url":"https://api.github.com/repos/OCA/server-tools/issues/comments{/number}","contents_url":"https://api.github.com/repos/OCA/server-tools/contents/{+path}","compare_url":"https://api.github.com/repos/OCA/server-tools/compare/{base}...{head}","merges_url":"https://api.github.com/repos/OCA/server-tools/merges","archive_url":"https://api.github.com/repos/OCA/server-tools/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/OCA/server-tools/downloads","issues_url":"https://api.github.com/repos/OCA/server-tools/issues{/number}","pulls_url":"https://api.github.com/repos/OCA/server-tools/pulls{/number}","milestones_url":"https://api.github.com/repos/OCA/server-tools/milestones{/number}","notifications_url":"https://api.github.com/repos/OCA/server-tools/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/OCA/server-tools/labels{/name}","releases_url":"https://api.github.com/repos/OCA/server-tools/releases{/id}","deployments_url":"https://api.github.com/repos/OCA/server-tools/deployments","created_at":"2014-06-16T18:52:44Z","updated_at":"2016-11-30T14:58:34Z","pushed_at":"2016-11-30T17:17:32Z","git_url":"git://github.com/OCA/server-tools.git","ssh_url":"git@github.com:OCA/server-tools.git","clone_url":"https://github.com/OCA/server-tools.git","svn_url":"https://github.com/OCA/server-tools","homepage":"https://odoo-community.org/project/tools-maintainers-30","size":18639,"stargazers_count":138,"watchers_count":138,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":412,"mirror_url":null,"open_issues_count":123,"forks":412,"open_issues":123,"watchers":138,"default_branch":"10.0"}},"_links":{"self":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/611"},"html":{"href":"https://github.com/OCA/server-tools/pull/611"},"issue":{"href":"https://api.github.com/repos/OCA/server-tools/issues/611"},"comments":{"href":"https://api.github.com/repos/OCA/server-tools/issues/611/comments"},"review_comments":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/611/comments"},"review_comment":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/OCA/server-tools/pulls/611/commits"},"statuses":{"href":"https://api.github.com/repos/OCA/server-tools/statuses/a5dc3af03dc984198cc884b69212557a6015d194"}}}}
{"actor":{"display_login":"lasley"}}