From 8b4ad65231e04a23ffb2a7a9b6d87a7c7c85b2fa Mon Sep 17 00:00:00 2001 From: anar Date: Oct 13 2017 12:13:13 +0000 Subject: [PATCH 1/4] Changed the way validation occurs. Implemented GithubRepo and GithubOrg validators and their tests. --- diff --git a/hubs/tests/test_widget_validators.py b/hubs/tests/test_widget_validators.py index d421aa5..96d887f 100644 --- a/hubs/tests/test_widget_validators.py +++ b/hubs/tests/test_widget_validators.py @@ -30,7 +30,6 @@ class ValidatorsTest(APPTest): self.assertRaises( ValueError, validators.Username, "nobody") - @unittest.skip("Not implemented yet") def test_github_organization(self): self.assertEqual( validators.GithubOrganization("fedora-infra"), @@ -40,12 +39,14 @@ class ValidatorsTest(APPTest): validators.GithubOrganization, "something-that-does-not-exist") - @unittest.skip("Not implemented yet") def test_github_repo(self): self.assertEqual( - validators.GithubRepo("fedmsg"), "fedmsg") + validators.GithubRepo('/'.join(["fedora-infra", + "fedmsg"])), + "fedmsg") self.assertRaises(ValueError, validators.GithubRepo, - "something-that-does-not-exist") + '/'.join(["fedora-infra", + "something-that-does-not-exist"])) def test_fmncontext(self): self.assertEqual(validators.FMNContext("email"), "email") diff --git a/hubs/utils/github.py b/hubs/utils/github.py index 47c1018..71ef257 100644 --- a/hubs/utils/github.py +++ b/hubs/utils/github.py @@ -9,6 +9,30 @@ import requests log = logging.getLogger(__name__) +def github_org_is_valid(token, username): + log.info("Finding github organization for {}".format(username)) + tmpl = "https://api.github.com/users/{username}" + url = tmpl.format(username=username) + auth = dict(access_token=token) + result = requests.get(url, params=auth) + if not bool(result): + return False + else: + return True + + +def github_repo_is_valid(token, username, repo): + log.info("Finding github repo for {} and {} ".format(repo, username)) + tmpl = "https://api.github.com/repos/{username}/{repo}" + url = tmpl.format(username=username, repo=repo) + auth = dict(access_token=token) + result = requests.get(url, params=auth) + if not bool(result): + return False + else: + return True + + def github_repos(token, username): log.info("Finding github repos for %r" % username) tmpl = "https://api.github.com/users/{username}/repos?per_page=100" diff --git a/hubs/utils/views.py b/hubs/utils/views.py index 2508dad..5e7581b 100644 --- a/hubs/utils/views.py +++ b/hubs/utils/views.py @@ -118,16 +118,17 @@ def configure_widget_instance(widget_instance, widget_config): """ if not widget_config: return - config = {} + vals = {} for param in widget_instance.module.get_parameters(): val = widget_config.get(param.name) if not val: raise WidgetConfigError( 'You must provide a value for: %s' % param.name) - try: - config[param.name] = param.validator(val) - except ValueError as err: - raise WidgetConfigError('Invalid data provided, error: %s' % err) + vals[param.name] = val + try: + config = widget_instance.module.validate_parameters(vals) + except ValueError as err: + raise WidgetConfigError('Invalid data provided, error: %s' % err) # Updating in-place is not supported, it's a class property. cur_config = widget_instance.config or {} cur_config = cur_config.copy() diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index db63192..42a1965 100644 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -99,7 +99,15 @@ class Widget(object): # Try to be smart-ish with the default label. self.label = self.name.replace("_", " ").replace(".", ": ").title() self._template_environment = None +# + def validate_parameters(self, vals): + config = {} + for param in self.get_parameters(): + config[param.name] = param.validator(vals[param.name]) + return config + +# def validate(self): """ Ensure that the widget has the bits it needs. diff --git a/hubs/widgets/githubissues/__init__.py b/hubs/widgets/githubissues/__init__.py index fa0c996..8334c23 100644 --- a/hubs/widgets/githubissues/__init__.py +++ b/hubs/widgets/githubissues/__init__.py @@ -34,6 +34,17 @@ class GitHubIssues(Widget): help="The number of tickets to display.", )] + def validate_parameters(self, vals): + config = {} + config["org"] = \ + validators.GithubOrganization(vals["org"]) + config["repo"] = \ + validators.GithubRepo('/'.join([vals["org"], + vals["repo"]])) + config["display_number"] = \ + validators.Integer(vals["display_number"]) + return config + class BaseView(RootWidgetView): diff --git a/hubs/widgets/validators.py b/hubs/widgets/validators.py index f8c5b8f..75257ba 100644 --- a/hubs/widgets/validators.py +++ b/hubs/widgets/validators.py @@ -10,6 +10,8 @@ will return the validated value. """ from __future__ import unicode_literals +from hubs.utils.github import github_org_is_valid, github_repo_is_valid +from hubs.utils import get_fedmsg_config import flask import hubs.models @@ -17,6 +19,8 @@ import kitchen.text.converters import requests import six +fedmsg_config = get_fedmsg_config() + def Required(value): """Raises an error if the value is ``False``-like.""" @@ -61,14 +65,19 @@ def Username(value): def GithubOrganization(value): """Fails if the Github organization name does not exist.""" - # TODO -- implement this. + token = fedmsg_config.get('github.oauth_token') + if not github_org_is_valid(token, value): + raise ValueError('Github organization does not exist') return value def GithubRepo(value): """Fails if the Github repository name does not exist.""" - # TODO -- implement this. - return value + token = fedmsg_config.get('github.oauth_token') + username, repo = value.split('/') + if not github_repo_is_valid(token, username, repo): + raise ValueError('Github repository does not exist') + return repo def FMNContext(value): From 8d9d32826a030b8dbf6e5bf041397ab10539dd34 Mon Sep 17 00:00:00 2001 From: anar Date: Oct 13 2017 12:13:13 +0000 Subject: [PATCH 2/4] Some minor polishing --- diff --git a/hubs/utils/github.py b/hubs/utils/github.py index 71ef257..78880de 100644 --- a/hubs/utils/github.py +++ b/hubs/utils/github.py @@ -9,25 +9,23 @@ import requests log = logging.getLogger(__name__) -def github_org_is_valid(token, username): +def github_org_is_valid(username): log.info("Finding github organization for {}".format(username)) tmpl = "https://api.github.com/users/{username}" url = tmpl.format(username=username) - auth = dict(access_token=token) - result = requests.get(url, params=auth) - if not bool(result): + result = requests.get(url) + if not result.ok: return False else: return True -def github_repo_is_valid(token, username, repo): +def github_repo_is_valid(username, repo): log.info("Finding github repo for {} and {} ".format(repo, username)) tmpl = "https://api.github.com/repos/{username}/{repo}" url = tmpl.format(username=username, repo=repo) - auth = dict(access_token=token) - result = requests.get(url, params=auth) - if not bool(result): + result = requests.get(url) + if not result.ok: return False else: return True diff --git a/hubs/utils/views.py b/hubs/utils/views.py index 5e7581b..0fe1e2b 100644 --- a/hubs/utils/views.py +++ b/hubs/utils/views.py @@ -118,15 +118,15 @@ def configure_widget_instance(widget_instance, widget_config): """ if not widget_config: return - vals = {} + values = {} for param in widget_instance.module.get_parameters(): val = widget_config.get(param.name) if not val: raise WidgetConfigError( 'You must provide a value for: %s' % param.name) - vals[param.name] = val + values[param.name] = val try: - config = widget_instance.module.validate_parameters(vals) + config = widget_instance.module.validate_parameters(values) except ValueError as err: raise WidgetConfigError('Invalid data provided, error: %s' % err) # Updating in-place is not supported, it's a class property. diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index 42a1965..eae34cc 100644 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -10,7 +10,6 @@ from importlib import import_module from .caching import CachedFunction from .view import WidgetView - log = logging.getLogger(__name__) @@ -99,15 +98,17 @@ class Widget(object): # Try to be smart-ish with the default label. self.label = self.name.replace("_", " ").replace(".", ": ").title() self._template_environment = None -# - def validate_parameters(self, vals): + def validate_parameters(self, values): + """ + Get a dictionary of values associated with corresponding parameters, + validate them and return. + """ config = {} for param in self.get_parameters(): - config[param.name] = param.validator(vals[param.name]) + config[param.name] = param.validator(values[param.name]) return config -# def validate(self): """ Ensure that the widget has the bits it needs. @@ -281,10 +282,11 @@ class Widget(object): "api_hub_widget", hub=instance.hub.name, idx=instance.idx), "config": {}, }) + validated = self.validate_parameters(instance.config) for param in self.get_parameters(): if param.secret and not with_secret_config: continue - value = param.validator(instance.config[param.name]) + value = validated[param.name] props["config"][param.name] = value if self.is_react: props["component"] = self.name diff --git a/hubs/widgets/validators.py b/hubs/widgets/validators.py index 75257ba..77504b9 100644 --- a/hubs/widgets/validators.py +++ b/hubs/widgets/validators.py @@ -11,7 +11,6 @@ will return the validated value. from __future__ import unicode_literals from hubs.utils.github import github_org_is_valid, github_repo_is_valid -from hubs.utils import get_fedmsg_config import flask import hubs.models @@ -19,8 +18,6 @@ import kitchen.text.converters import requests import six -fedmsg_config = get_fedmsg_config() - def Required(value): """Raises an error if the value is ``False``-like.""" @@ -65,17 +62,15 @@ def Username(value): def GithubOrganization(value): """Fails if the Github organization name does not exist.""" - token = fedmsg_config.get('github.oauth_token') - if not github_org_is_valid(token, value): + if not github_org_is_valid(value): raise ValueError('Github organization does not exist') return value def GithubRepo(value): """Fails if the Github repository name does not exist.""" - token = fedmsg_config.get('github.oauth_token') username, repo = value.split('/') - if not github_repo_is_valid(token, username, repo): + if not github_repo_is_valid(username, repo): raise ValueError('Github repository does not exist') return repo From fd41c203324d0399901971f6c98c929d48310996 Mon Sep 17 00:00:00 2001 From: anar Date: Oct 13 2017 12:13:13 +0000 Subject: [PATCH 3/4] Addressed comments --- diff --git a/hubs/utils/github.py b/hubs/utils/github.py index 78880de..5da725a 100644 --- a/hubs/utils/github.py +++ b/hubs/utils/github.py @@ -14,10 +14,7 @@ def github_org_is_valid(username): tmpl = "https://api.github.com/users/{username}" url = tmpl.format(username=username) result = requests.get(url) - if not result.ok: - return False - else: - return True + return result.ok def github_repo_is_valid(username, repo): @@ -25,10 +22,7 @@ def github_repo_is_valid(username, repo): tmpl = "https://api.github.com/repos/{username}/{repo}" url = tmpl.format(username=username, repo=repo) result = requests.get(url) - if not result.ok: - return False - else: - return True + return result.ok def github_repos(token, username): diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index eae34cc..bc10b01 100644 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -101,8 +101,14 @@ class Widget(object): def validate_parameters(self, values): """ - Get a dictionary of values associated with corresponding parameters, - validate them and return. + Validate the parameters of a widget. + + Args: + values (dict): dictionary of values associated + with corresponding parameters. + + Returns: + dict: validated parameter values. """ config = {} for param in self.get_parameters(): diff --git a/hubs/widgets/githubissues/__init__.py b/hubs/widgets/githubissues/__init__.py index 8334c23..c8c2956 100644 --- a/hubs/widgets/githubissues/__init__.py +++ b/hubs/widgets/githubissues/__init__.py @@ -34,15 +34,15 @@ class GitHubIssues(Widget): help="The number of tickets to display.", )] - def validate_parameters(self, vals): + def validate_parameters(self, values): config = {} config["org"] = \ - validators.GithubOrganization(vals["org"]) + validators.GithubOrganization(values["org"]) config["repo"] = \ - validators.GithubRepo('/'.join([vals["org"], - vals["repo"]])) + validators.GithubRepo('/'.join([values["org"], + values["repo"]])) config["display_number"] = \ - validators.Integer(vals["display_number"]) + validators.Integer(values["display_number"]) return config From f0f2a78d5854ad34dc63f67e3fc325d3241645f4 Mon Sep 17 00:00:00 2001 From: anar Date: Oct 18 2017 15:09:20 +0000 Subject: [PATCH 4/4] Added files created by testing --- diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widget_validators.ValidatorsTest.test_github_organization b/hubs/tests/vcr-request-data/hubs.tests.test_widget_validators.ValidatorsTest.test_github_organization new file mode 100644 index 0000000..dcfc6ae --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widget_validators.ValidatorsTest.test_github_organization @@ -0,0 +1,86 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.18.4] + method: GET + uri: https://api.github.com/users/fedora-infra + response: + body: + string: !!binary | + H4sIAAAAAAAAA52TzW6cMBSFXyXyehjjMJ02lqruKnXVzayyGRnjgRsZ2/IP0RTl3XsNJJogVRWz + Aiyf7x6OfUaibQuGcHJRjfWiAHPxguwINIRXFTseq687IgYRhT8nr3FjF6MLnNJ5MbB9C7FLdQrK + S2uiMnEvbU8TXeQ/hu8HBLZ+oWQywYUVzcECmtVIC3TlqYu9XpmYZ0+S1eaL1dq+ImVt+3+D6IcS + Tc7vYNo7KagcqY2dwvTwl95yEBDidlOTaqT5cYYmcwIeiVfNZmOLDm29GnQ0Uq+cnYCpDtKDi2DN + doOf1EizvhUG/oj7aKgOCMnWtluZVKhWA17G7fJZNlLnYRDymqPxSioYMOw7kSs9EuPVKezB75uU + 8hFAVGfR9LmRF6GD2hEj+rzx51TPh1+5niH6JGPyChV4850wV8JN0npHaqzzbUedC/u5GM7bFyXj + HpOlqNNWTkfzLlS9AGz3TOnAK1FrnLtQwf7LwsNJiR55LtUa5HmOnrOSfSxNN5fw8r1MWMmbLyzI + 9CVxYsSARcRJjyWripIV7OnEnvjhGz98ecYZyTWf9hyLsirY4fRY8orxsnomb38BYQwDb9AEAAA= + headers: + access-control-allow-origin: ['*'] + access-control-expose-headers: ['ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, + X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval'] + cache-control: ['public, max-age=60, s-maxage=60'] + content-encoding: [gzip] + content-security-policy: [default-src 'none'] + content-type: [application/json; charset=utf-8] + date: ['Wed, 18 Oct 2017 14:57:08 GMT'] + etag: [W/"26ff4247338ae3b104803156575c2cff"] + expect-ct: ['max-age=2592000; report-uri="https://api.github.com/_private/browser/errors"'] + last-modified: ['Mon, 14 Mar 2016 20:31:03 GMT'] + server: [GitHub.com] + status: [200 OK] + strict-transport-security: [max-age=31536000; includeSubdomains; preload] + vary: [Accept] + x-content-type-options: [nosniff] + x-frame-options: [deny] + x-github-media-type: [github.v3; format=json] + x-github-request-id: ['8483:248DD:3178E2B:58E7F3D:59E76BC3'] + x-ratelimit-limit: ['60'] + x-ratelimit-remaining: ['39'] + x-ratelimit-reset: ['1508341503'] + x-runtime-rack: ['0.020675'] + x-xss-protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.18.4] + method: GET + uri: https://api.github.com/users/something-that-does-not-exist + response: + body: + string: !!binary | + H4sIAAAAAAAAAxXJMQ7CMAwF0Ksgs5J6YOsBGHsFFJqvNFISV7HdBfXuhfW9LzWoxgyaaRG7vcR7 + ogclWb2hW7Qi/e2j/n4z23VmTjhQZceYcrHNP9MqjY8nu2Io3zMsxKCl54rwNzovaRNpS2YAAAA= + headers: + access-control-allow-origin: ['*'] + access-control-expose-headers: ['ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, + X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval'] + content-encoding: [gzip] + content-security-policy: [default-src 'none'] + content-type: [application/json; charset=utf-8] + date: ['Wed, 18 Oct 2017 14:57:09 GMT'] + expect-ct: ['max-age=2592000; report-uri="https://api.github.com/_private/browser/errors"'] + server: [GitHub.com] + status: [404 Not Found] + strict-transport-security: [max-age=31536000; includeSubdomains; preload] + x-content-type-options: [nosniff] + x-frame-options: [deny] + x-github-media-type: [github.v3; format=json] + x-github-request-id: ['9928:248DC:1970632:35564D7:59E76BC4'] + x-ratelimit-limit: ['60'] + x-ratelimit-remaining: ['38'] + x-ratelimit-reset: ['1508341503'] + x-runtime-rack: ['0.013258'] + x-xss-protection: [1; mode=block] + status: {code: 404, message: Not Found} +version: 1 diff --git a/hubs/tests/vcr-request-data/hubs.tests.test_widget_validators.ValidatorsTest.test_github_repo b/hubs/tests/vcr-request-data/hubs.tests.test_widget_validators.ValidatorsTest.test_github_repo new file mode 100644 index 0000000..575ba8d --- /dev/null +++ b/hubs/tests/vcr-request-data/hubs.tests.test_widget_validators.ValidatorsTest.test_github_repo @@ -0,0 +1,99 @@ +interactions: +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.18.4] + method: GET + uri: https://api.github.com/repos/fedora-infra/fedmsg + response: + body: + string: !!binary | + H4sIAAAAAAAAA+1Yy27jOBD8lUDXdUzbSjIZAYvZ094GuwvkNBeDlmiJE4kUSMpGIuTft0jqZQN+ + hL4GCBybYBWLTXazu9uIZ1ESx6vv8fPTLBK0YlESbVlW6TyaRdumLNfjoFT0noutomSYIfeCqShp + o1LmXHjsMA0Mnn759BR/m0V0Rw1V60aVmFgYU+uEED+ol/Ocm6LZNJqpVArDhJmnsiINiWMH/7H7 + 8wGEuepYLHOEgSO2mndEHg02bdVONRWmKo9E+LUd5GjyVpal3IPlWPalhciAtIZ0LFzkgSxAtkSa + gsF62NKHNQTX5vOiHKol9t+aZ5ZH40gUyz4trMNBlr0CHy1RrJaOsNnoVPHacCk+L/AADTapcir4 + Ow1jA1qDxEr7vBSHAprtcBk/D/ewltSK72j6Zk2jWMr4DsYOpDzCg9G81dZj/5lYyR4BN2xNs8p6 + 5JaWmn3MIifDYLIbmMH/rvWC0dszNpwsFv2bZUyBMrv7ybSm8P/8bg8nvvvFlPz5H3RspXodFjzr + qM7WB743rmpZLpj/JBxeCDCkvLK3YA6LbQk+O5dJ4c10g5Bi5KW4cFrYAUlLpj/tVTGMVsGCHRgk + hZThlnNgkHCtG3bVjT29WcehSe8Soqk2Popd4winaT0aGqnWPBeMBVtsIGhJH2A3ioq0CKfs8S3x + 39yp0jxYosWCYlPKTTAH3jniCFqiC+qfEbO+RZVltPgDQsW2N0m0+IHQqBvO1cmzBAMd3i2DIw7W + 1+NJ21mwpCJvaB7OOBDgdO2rmtP3i/nGaZ8YGUBnMynFN81tgWrksAr98w7/DTfhSDESunzhfBZy + ZtOTnMNtu6r4pSf7NFsHP7jSN1Lae3hMa39fzizOy7T4lozx1AfrjjnUml207vVN+bvUPPjoezxp + /6ipKWwEwjI1VSxUbAcn7YYiz5nP523BqMtsK6Zu8EqPBg1VaYGkLVRf2+ORiVTUuER5a+VlSJxL + SbNgWw4EIPNHFqrRo6fnXKPyCxbmwFO2ipdMGynCY+TIMOUV0vAtT68pDk670QFJ+0NzkbIZLcsZ + bqXhKcc9RWZrTwxJHwu3ikdDPopsXwyUDFc22MqKeXxLfCGXsbqUbzdFlAmFdUzFbHa/pgbJ/mqx + XN0v8Ld8WS2Tx0WyfP6FOU2dHcz5dr9c3C8eXxarZPGQxAs7p250MaFxU5bxywocj8niyU5BeOzu + Lr6hKXCiHu9qAlvhA6R1MYL+GiHJtIQ/hKQlLuGRl1y31u74bToPg7xCVqxGXjDpdfi2yRx2zVDL + ZzLVcy6J3Qp/x7zV9+f4IAVIZSNg/OUDhvfUIBvFozsd7FMHrPHvmymksOtSvfYOHSVGNbbUw0it + 5G+WGj0dGwPIZOKev/KxSLRIm9sMI74Y6zTYnlHFlZJdU0fA8VGy10x0CnqxMTbgK7HEYiYzbPdp + 3Fy314xtaVOatU+csbkMmX4pa+xu2g/46ju5YuW4bu47VrDWV9+pazJe7NZ99Z3QfD3RM0XGctC3 + wsW6vu8kmNmjB9NHAuv+0xKiiyTx6uN/dyzNvYwWAAA= + headers: + access-control-allow-origin: ['*'] + access-control-expose-headers: ['ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, + X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval'] + cache-control: ['public, max-age=60, s-maxage=60'] + content-encoding: [gzip] + content-security-policy: [default-src 'none'] + content-type: [application/json; charset=utf-8] + date: ['Wed, 18 Oct 2017 14:57:10 GMT'] + etag: [W/"38e039a20d5cd6cc6f07ddf3cecca325"] + expect-ct: ['max-age=2592000; report-uri="https://api.github.com/_private/browser/errors"'] + last-modified: ['Thu, 05 Oct 2017 02:04:30 GMT'] + server: [GitHub.com] + status: [200 OK] + strict-transport-security: [max-age=31536000; includeSubdomains; preload] + vary: [Accept] + x-content-type-options: [nosniff] + x-frame-options: [deny] + x-github-media-type: [github.v3; format=json] + x-github-request-id: ['B0BB:248DD:3178F42:58E812F:59E76BC5'] + x-ratelimit-limit: ['60'] + x-ratelimit-remaining: ['37'] + x-ratelimit-reset: ['1508341503'] + x-runtime-rack: ['0.039710'] + x-xss-protection: [1; mode=block] + status: {code: 200, message: OK} +- request: + body: null + headers: + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Connection: [keep-alive] + User-Agent: [python-requests/2.18.4] + method: GET + uri: https://api.github.com/repos/fedora-infra/something-that-does-not-exist + response: + body: + string: !!binary | + H4sIAAAAAAAAA6tWyk0tLk5MT1WyUvLLL1Fwyy/NS1HSUUrJTy7NTc0rSSzJzM+LLy3KAcpnlJQU + FFvp66eklqXm5BekFumlZ5ZklCbpJefn6pcZK9UCAP6TTUJNAAAA + headers: + access-control-allow-origin: ['*'] + access-control-expose-headers: ['ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, + X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval'] + content-encoding: [gzip] + content-security-policy: [default-src 'none'] + content-type: [application/json; charset=utf-8] + date: ['Wed, 18 Oct 2017 14:57:10 GMT'] + expect-ct: ['max-age=2592000; report-uri="https://api.github.com/_private/browser/errors"'] + server: [GitHub.com] + status: [404 Not Found] + strict-transport-security: [max-age=31536000; includeSubdomains; preload] + x-content-type-options: [nosniff] + x-frame-options: [deny] + x-github-media-type: [github.v3; format=json] + x-github-request-id: ['5100:248DD:3178FF1:58E825A:59E76BC6'] + x-ratelimit-limit: ['60'] + x-ratelimit-remaining: ['36'] + x-ratelimit-reset: ['1508341503'] + x-runtime-rack: ['0.018622'] + x-xss-protection: [1; mode=block] + status: {code: 404, message: Not Found} +version: 1