Enhancement: Allow chaining request and responses with shared url state

This commit is contained in:
Kyle Hornberg
2018-02-27 08:41:58 -06:00
parent 3f9b794033
commit 37e983961f
4 changed files with 105 additions and 14 deletions
+1 -1
View File
@@ -129,7 +129,7 @@ app::
octokit = Octokit(auth='app', app_id=42, private_key=private_key)
app::
app installation::
octokit = Octokit(auth='installation', app_id=42, private_key=private_key)
+36 -1
View File
@@ -4,4 +4,39 @@ Usage
To use octokit.py in a project::
import octokit.py
import octokit
Chaining requests
=================
::
issue = Octokit().issues.edit(owner='testUser', repo='testRepo', number=1, state='closed')
# If the previous request had a required url attribute, the next request will use the previous url attribute
# This does not apply attributes that are part of the body of the request on post, patch, etc.
issue.pull_requests.create(head='branch', base='master', title='Title')
# Previous attributes can be overridden
issue.pull_requests.create(owner='differentOwner', head='branch', base='master', title='Title')
Responses
=========
Responses are the Octokit instance with state in ``json`` and ``response``. ``json`` is the result of the Requests ``response.json()``. ``response`` is the json as a python object.
octokit.json
================
::
issue = Octokit().issues.get(owner='testUser', repo='testRepo', number=1)
issue.json['title'] # Title of issue
octokit.response
================
::
issue = Octokit().issues.get(owner='testUser', repo='testRepo', number=1)
issue.response.title # Title of issue
+12 -8
View File
@@ -1,7 +1,7 @@
import datetime
import json
import re
from collections import ChainMap
from collections import ChainMap, defaultdict
import requests
from jose import jwt
@@ -17,9 +17,10 @@ class Base(object):
return dict(ChainMap(definition.get('headers', {}), self.headers))
def _validate(self, kwargs, params):
cached_kwargs = dict(ChainMap(kwargs, self._attribute_cache['url']))
required_params = [k for k, v in params.items() if v.get('required')]
for p in required_params:
assert p in kwargs # has all required
assert p in cached_kwargs # has all required
for kwarg, value in kwargs.items():
param_value = params.get(kwarg)
assert param_value # is a valid param but not necessarily required
@@ -28,12 +29,14 @@ class Base(object):
if kwarg in required_params:
assert value # required param has a value
def _form_url(self, values, _url):
data_values = values.copy()
for name, value in values.items():
def _form_url(self, values, _url, params):
_values = dict(ChainMap(values, self._attribute_cache['url']))
filtered_kwargs = {k: v for k, v in _values.items() if params.get(k)}
data_values = filtered_kwargs.copy()
for name, value in filtered_kwargs.items():
_url, subs = re.subn(':{}'.format(name), str(value), _url)
if subs != 0:
data_values.pop(name)
self._attribute_cache['url'][name] = data_values.pop(name)
url = '{}{}'.format(self.base_url, _url)
return url, data_values
@@ -48,7 +51,7 @@ class Base(object):
if method == 'get':
return {'params': data}
if method in ['post', 'patch', 'put', 'delete']:
return {'data': json.dumps(data)}
return {'data': json.dumps(data, sort_keys=True)}
return {}
def _setup_authentication(self, kwargs):
@@ -126,6 +129,7 @@ class Octokit(Base):
def __init__(self, *args, **kwargs):
self._create(utils.get_json_data('rest.json'))
self._setup_authentication(kwargs)
self._attribute_cache = defaultdict(dict)
def _create(self, definitions):
for name, value in definitions.items():
@@ -148,7 +152,7 @@ class Octokit(Base):
self._validate(kwargs, definition.get('params'))
method = definition['method'].lower()
requests_kwargs = {'headers': self._get_headers(definition)}
url, data_kwargs = self._form_url(kwargs, definition['url'])
url, data_kwargs = self._form_url(kwargs, definition['url'], definition.get('params'))
requests_kwargs.update(self._data(data_kwargs, definition.get('params'), method))
requests_kwargs.update(self._auth(requests_kwargs))
_response = getattr(requests, method)(url, **requests_kwargs)
+56 -4
View File
@@ -10,7 +10,11 @@ class TestClientMethods(object):
def test_client_methods_are_lower_case(self):
for client in Octokit().__dict__:
assert all(method.islower() for method in getattr(Octokit(), client).__dict__)
try:
cls = getattr(Octokit(), client).__dict__
except AttributeError:
pass # ignore non-class attributes
assert all(method.islower() for method in cls)
def test_method_has_doc_string(self):
assert Octokit().authorization.get.__doc__ == 'Get a single authorization.'
@@ -43,10 +47,12 @@ class TestClientMethods(object):
def test_request_has_body_parameters(self, mocker):
mocker.patch('requests.post')
data = {'scopes': ['public_repo'], 'note': 'admin script'}
Octokit().authorization.create(**data)
data = {'scopes': ['public_repo']}
create = Octokit().authorization.create(**data)
requests.post.assert_called_once_with(
'https://api.github.com/authorizations', data=json.dumps(data), headers=Octokit().headers
'https://api.github.com/authorizations',
data=json.dumps(data),
headers=create.headers
)
def test_must_include_required_body_parameters(self):
@@ -84,6 +90,52 @@ class TestClientMethods(object):
'https://api.github.com/applications/grants/404', params=params, headers=Octokit().headers
)
def test_use_previous_values_if_available(self, mocker):
mocker.patch('requests.patch')
mocker.patch('requests.post')
headers = {'accept': 'application/vnd.github.squirrel-girl-preview', 'Content-Type': 'application/json'}
data = {'state': 'closed'}
issue = Octokit().issues.edit(owner='testUser', repo='testRepo', number=1, **data)
requests.patch.assert_called_with(
'https://api.github.com/repos/testUser/testRepo/issues/1', data=json.dumps(data), headers=headers
)
issue2 = issue.issues.edit(**data)
requests.patch.assert_called_with(
'https://api.github.com/repos/testUser/testRepo/issues/1',
data=json.dumps(data),
headers=headers
)
issue2.pull_requests.create(head='branch', base='master', title='Title')
requests.post.assert_called_with(
'https://api.github.com/repos/testUser/testRepo/pulls',
data=json.dumps({
'base': 'master',
'head': 'branch',
'title': 'Title',
}, sort_keys=True),
headers={'Content-Type': 'application/json', 'accept': 'application/vnd.github.machine-man-preview+json'}
)
def test_can_override_previous_values(self, mocker):
mocker.patch('requests.patch')
mocker.patch('requests.post')
headers = {'accept': 'application/vnd.github.squirrel-girl-preview', 'Content-Type': 'application/json'}
data = {'state': 'closed'}
issue = Octokit().issues.edit(owner='testUser', repo='testRepo', number=1, **data)
requests.patch.assert_called_with(
'https://api.github.com/repos/testUser/testRepo/issues/1', data=json.dumps(data), headers=headers
)
issue.pull_requests.create(owner='user', head='branch', base='master', title='Title')
requests.post.assert_called_with(
'https://api.github.com/repos/user/testRepo/pulls',
data=json.dumps({
'base': 'master',
'head': 'branch',
'title': 'Title',
}, sort_keys=True),
headers={'Content-Type': 'application/json', 'accept': 'application/vnd.github.machine-man-preview+json'}
)
def test_returned_object_is_self(self, mocker):
mocker.patch('requests.patch')
headers = {'accept': 'application/vnd.github.squirrel-girl-preview', 'Content-Type': 'application/json'}