Skip to content
Snippets Groups Projects
Commit 2e35d727 authored by Alexandre Detiste's avatar Alexandre Detiste
Browse files

New upstream version 0.9.1

parent 12e33eaf
No related branches found
No related tags found
No related merge requests found
......@@ -2,7 +2,7 @@ language: python
python:
- '2.7'
- '3.5'
install: pip install .
install: pip install .[tests]
script: python -m phabricator.tests.test_phabricator
deploy:
provider: pypi
......
0.9.1
* Update requests version.
0.9.0
* Add default retry to API calls.
0.8.1
* Fixed bug where built-in interface dict overwrote `interface` methods from Almanac (`interface.search`, `interface.edit`). The `Resource` attribute `interface` is renamed `_interface`.
0.8.0
* Switch to using requests
* Allow library to be used from a zip archive
* Deprecated parameters are now set to optional
* Fixed bug where instance attribute was shadowing API endpoint
* Update interfaces.json
* Update README
0.7.0
* Allow for nested methods to support conduit calls such as diffusion.repository.edit
......
......@@ -20,6 +20,10 @@ Use the API by instantiating it, and then calling the method through dotted nota
phab = Phabricator() # This will use your ~/.arcrc file
phab.user.whoami()
You can also use::
phab = Phabricator(host='https://my-phabricator.org/api/', token='api-mytoken')
Parameters are passed as keyword arguments to the resource call::
phab.user.find(aliases=["sugarc0de"])
......
#!/bin/bash
set -eo pipefail
python setup.py sdist bdist_wheel
twine upload dist/*
\ No newline at end of file
......@@ -21,10 +21,13 @@ import json
import os.path
import re
import socket
import pkgutil
import time
import requests
from requests.adapters import HTTPAdapter, Retry
from ._compat import (
MutableMapping, iteritems, string_types, httplib, urlparse, urlencode,
MutableMapping, iteritems, string_types, urlencode,
)
......@@ -37,8 +40,9 @@ CURRENT_DIR = os.getcwd()
# Default Phabricator interfaces
INTERFACES = {}
with open(os.path.join(os.path.dirname(__file__), 'interfaces.json')) as fobj:
INTERFACES = json.load(fobj)
INTERFACES = json.loads(
pkgutil.get_data(__name__, 'interfaces.json')
.decode('utf-8'))
# Load arc config
......@@ -170,6 +174,8 @@ def parse_interfaces(interfaces):
param_type = 'string'
elif info_piece == 'nonempty':
optionality = 'required'
elif info_piece == 'deprecated':
optionality = 'optional'
else:
param_type = info_piece
......@@ -219,15 +225,24 @@ class Result(MutableMapping):
class Resource(object):
def __init__(self, api, interface=None, endpoint=None, method=None, nested=False):
self.api = api
self.interface = interface or copy.deepcopy(parse_interfaces(INTERFACES))
self._interface = interface or copy.deepcopy(parse_interfaces(INTERFACES))
self.endpoint = endpoint
self.method = method
self.nested = nested
self.session = requests.Session()
adapter = HTTPAdapter(max_retries=Retry(
total=3,
connect=3,
allowed_methods=["HEAD", "GET", "POST", "PATCH", "PUT", "OPTIONS"]
))
self.session.mount("https://", adapter)
self.session.mount("http://", adapter)
def __getattr__(self, attr):
if attr in getattr(self, '__dict__'):
return getattr(self, attr)
interface = self.interface
interface = self._interface
if self.nested:
attr = "%s.%s" % (self.endpoint, attr)
submethod_exists = False
......@@ -249,7 +264,7 @@ class Resource(object):
def _request(self, **kwargs):
# Check for missing variables
resource = self.interface
resource = self._interface
def validate_kwarg(key, target):
# Always allow list
......@@ -286,41 +301,28 @@ class Resource(object):
self.api.connect()
kwargs['__conduit__'] = self.api._conduit
url = urlparse.urlparse(self.api.host)
if url.scheme == 'https':
conn = httplib.HTTPSConnection(url.netloc, timeout=self.api.timeout)
else:
conn = httplib.HTTPConnection(url.netloc, timeout=self.api.timeout)
path = url.path + '%s.%s' % (self.method, self.endpoint)
headers = {
'User-Agent': 'python-phabricator/%s' % str(self.api.clientVersion),
'Content-Type': 'application/x-www-form-urlencoded'
}
body = urlencode({
body = {
"params": json.dumps(kwargs),
"output": self.api.response_format
})
}
# TODO: Use HTTP "method" from interfaces.json
conn.request('POST', path, body, headers)
response = conn.getresponse()
path = '%s%s.%s' % (self.api.host, self.method, self.endpoint)
response = self.session.post(path, data=body, headers=headers, timeout=self.api.timeout)
# Make sure we got a 2xx response indicating success
if not response.status >= 200 or not response.status < 300:
raise httplib.HTTPException(
'Bad response status: {0}'.format(response.status)
if not response.status_code >= 200 or not response.status_code < 300:
raise requests.exceptions.HTTPError(
'Bad response status: {0}'.format(response.status_code)
)
response_data = response.read()
if isinstance(response_data, str):
response = response_data
else:
response = response_data.decode("utf-8")
data = self._parse_response(response)
data = self._parse_response(response.text)
return Result(data['result'])
......@@ -351,9 +353,9 @@ class Phabricator(Resource):
raise ConfigurationError("No host found or provided.")
current_host_config = defined_hosts.get(self.host, {})
self.token = token if token else current_host_config.get('token')
self.conduit_token = token if token else current_host_config.get('token')
if self.token is None:
if self.conduit_token is None:
self.username = username if username else current_host_config.get('user')
self.certificate = certificate if certificate else current_host_config.get('cert')
......@@ -364,15 +366,15 @@ class Phabricator(Resource):
self.clientDescription = socket.gethostname() + ':python-phabricator'
self._conduit = None
super(Phabricator, self).__init__(self)
super(Phabricator, self).__init__(self, **kwargs)
def _request(self, **kwargs):
raise SyntaxError('You cannot call the Conduit API without a resource.')
def connect(self):
if self.token:
if self.conduit_token:
self._conduit = {
'token': self.token
'token': self.conduit_token
}
return
......@@ -399,4 +401,4 @@ class Phabricator(Resource):
interfaces = query()
self.interface = parse_interfaces(interfaces)
self._interface = parse_interfaces(interfaces)
......@@ -7,16 +7,6 @@ try:
except ImportError:
from collections import MutableMapping
try:
import httplib
except ImportError:
import http.client as httplib
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
try:
from urllib import urlencode
except ImportError:
......
This diff is collapsed.
......@@ -3,20 +3,14 @@ try:
except ImportError:
import unittest
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
try:
import unittest.mock as mock
except ImportError:
import mock
import requests
import responses
from pkg_resources import resource_string
import json
import phabricator
phabricator.ARCRC = {} # overwrite any arcrc that might be read
RESPONSES = json.loads(
......@@ -40,7 +34,7 @@ class PhabricatorTest(unittest.TestCase):
self.api = phabricator.Phabricator(
username='test',
certificate='test',
host='http://localhost'
host='http://localhost/api/'
)
self.api.certificate = CERTIFICATE
......@@ -49,35 +43,32 @@ class PhabricatorTest(unittest.TestCase):
hashed = self.api.generate_hash(token)
self.assertEqual(hashed, 'f8d3bea4e58a2b2967d93d5b307bfa7c693b2e7f')
@mock.patch('phabricator.httplib.HTTPConnection')
def test_connect(self, mock_connection):
mock_obj = mock_connection.return_value = mock.Mock()
mock_obj.getresponse.return_value = StringIO(
RESPONSES['conduit.connect']
)
mock_obj.getresponse.return_value.status = 200
@responses.activate
def test_connect(self):
responses.add('POST', 'http://localhost/api/conduit.connect',
body=RESPONSES['conduit.connect'], status=200)
api = phabricator.Phabricator(
username='test',
certificate='test',
host='http://localhost'
host='http://localhost/api/'
)
api.connect()
keys = api._conduit.keys()
self.assertIn('sessionKey', keys)
self.assertIn('connectionID', keys)
assert len(responses.calls) == 1
@mock.patch('phabricator.httplib.HTTPConnection')
def test_user_whoami(self, mock_connection):
mock_obj = mock_connection.return_value = mock.Mock()
mock_obj.getresponse.return_value = StringIO(RESPONSES['user.whoami'])
mock_obj.getresponse.return_value.status = 200
@responses.activate
def test_user_whoami(self):
responses.add('POST', 'http://localhost/api/user.whoami',
body=RESPONSES['user.whoami'], status=200)
api = phabricator.Phabricator(
username='test',
certificate='test',
host='http://localhost'
host='http://localhost/api/'
)
api._conduit = True
......@@ -87,7 +78,7 @@ class PhabricatorTest(unittest.TestCase):
api = phabricator.Phabricator(
username='test',
certificate='test',
host='http://localhost'
host='http://localhost/api/'
)
self.assertEqual(api.user.whoami.method, 'user')
......@@ -97,40 +88,37 @@ class PhabricatorTest(unittest.TestCase):
api = phabricator.Phabricator(
username='test',
certificate='test',
host='http://localhost'
host='http://localhost/api/'
)
self.assertEqual(api.diffusion.repository.edit.method, 'diffusion')
self.assertEqual(api.diffusion.repository.edit.endpoint, 'repository.edit')
self.assertEqual(
api.diffusion.repository.edit.endpoint, 'repository.edit')
@mock.patch('phabricator.httplib.HTTPConnection')
def test_bad_status(self, mock_connection):
mock_obj = mock_connection.return_value = mock.Mock()
mock_obj.getresponse.return_value = mock.Mock()
mock_obj.getresponse.return_value.status = 400
@responses.activate
def test_bad_status(self):
responses.add(
'POST', 'http://localhost/api/conduit.connect', status=400)
api = phabricator.Phabricator(
username='test',
certificate='test',
host='http://localhost'
username='test',
certificate='test',
host='http://localhost/api/'
)
api._conduit = True
with self.assertRaises(phabricator.httplib.HTTPException):
with self.assertRaises(requests.exceptions.HTTPError):
api.user.whoami()
assert len(responses.calls) == 1
@mock.patch('phabricator.httplib.HTTPConnection')
def test_maniphest_find(self, mock_connection):
mock_obj = mock_connection.return_value = mock.Mock()
mock_obj.getresponse.return_value = StringIO(
RESPONSES['maniphest.find']
)
mock_obj.getresponse.return_value.status = 200
@responses.activate
def test_maniphest_find(self):
responses.add('POST', 'http://localhost/api/maniphest.find',
body=RESPONSES['maniphest.find'], status=200)
api = phabricator.Phabricator(
username='test',
certificate='test',
host='http://localhost'
host='http://localhost/api/'
)
api._conduit = True
......@@ -161,16 +149,26 @@ class PhabricatorTest(unittest.TestCase):
def test_map_param_type(self):
uint = 'uint'
self.assertEqual(phabricator.map_param_type(uint), int)
self.assertEqual(phabricator.map_param_type(uint), int)
list_bool = 'list<bool>'
self.assertEqual(phabricator.map_param_type(list_bool), [bool])
self.assertEqual(phabricator.map_param_type(list_bool), [bool])
list_pair = 'list<pair<callsign, path>>'
self.assertEqual(phabricator.map_param_type(list_pair), [tuple])
self.assertEqual(phabricator.map_param_type(list_pair), [tuple])
complex_list_pair = 'list<pair<string-constant<"gtcm">, string>>'
self.assertEqual(phabricator.map_param_type(complex_list_pair), [tuple])
self.assertEqual(phabricator.map_param_type(
complex_list_pair), [tuple])
def test_endpoint_shadowing(self):
shadowed_endpoints = [e for e in self.api._interface.keys() if e in self.api.__dict__]
self.assertEqual(
shadowed_endpoints,
[],
"The following endpoints are shadowed: {}".format(shadowed_endpoints)
)
if __name__ == '__main__':
unittest.main()
......@@ -4,7 +4,7 @@ import sys
from setuptools import setup, find_packages
tests_requires = []
tests_requires = ['responses>=0.12']
if sys.version_info[:2] < (2, 7):
tests_requires.append('unittest2')
......@@ -14,15 +14,18 @@ if sys.version_info[:2] <= (3, 3):
setup(
name='phabricator',
version='0.7.0',
version='0.9.1',
author='Disqus',
author_email='opensource@disqus.com',
url='http://github.com/disqus/python-phabricator',
description='Phabricator API Bindings',
packages=find_packages(),
zip_safe=False,
install_requires=['requests>=2.26'],
test_suite='phabricator.tests.test_phabricator',
tests_require=tests_requires,
extras_require={
'tests': tests_requires,
},
include_package_data=True,
classifiers=[
'Intended Audience :: Developers',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment