Skip to content

Commit 7676401

Browse files
committed
Merge branch 'xml-requests-for-creating-tickets' of git://github.com/aleg/codebase-python-api-client into aleg-xml-requests-for-creating-tickets
2 parents 1353b12 + 59748c0 commit 7676401

17 files changed

Lines changed: 1432 additions & 104 deletions

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ nosetests.xml
3333
.mr.developer.cfg
3434
.project
3535
.pydevproject
36+
37+
# Vim
38+
*.swp

‎Makefile‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
nosetests := nosetests \
2+
--no-byte-compile \
3+
--with-coverage \
4+
--cover-erase \
5+
--cover-package=codebase \
6+
--cover-branches \
7+
--cover-inclusive
8+
9+
runtests-install:
10+
@clear
11+
@echo Preflight tasks...
12+
@pip install -U pip >> /dev/null
13+
@pip install -r tests/requirements.txt >> /dev/null
14+
@find ./ -name "*.pyc" -delete >> /dev/null
15+
16+
@echo Running tests:
17+
@$(nosetests) --where=tests/unit $(specs)
18+
19+
runtests:
20+
@clear
21+
@echo Running tests:
22+
@$(nosetests) --where=tests/unit $(specs)
23+
24+
runtests-all:
25+
@clear
26+
@echo Running tests:
27+
@$(nosetests) $(specs)

‎codebase/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import logging
22

3-
logger = logging.getLogger(__name__)
3+
logger = logging.getLogger(__name__)

‎codebase/client.py‎

100755100644
Lines changed: 110 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,80 +3,99 @@
33
import logging
44
import urllib
55
import urllib2
6+
import urlparse
7+
8+
import xmltodict
69

710
from codebase import logger
11+
from codebase.settings import Settings
812

913

1014
class Auth(object):
11-
15+
CTYPE_JSON = 'json'
16+
CTYPE_XML = 'xml'
1217
API_ENDPOINT = 'https://api3.codebasehq.com'
1318

14-
def _default_settings(self):
15-
# prevent import error on AppEngine
16-
from codebase.settings import Settings
17-
settings = Settings()
18-
self.username = settings.CODEBASE_USERNAME
19-
self.apikey = settings.CODEBASE_APIKEY
20-
2119
def __init__(self, project=None, username=None, apikey=None, **kwargs):
2220
super(Auth, self).__init__(**kwargs)
2321

24-
if username and apikey:
25-
self.username = username
26-
self.apikey = apikey
27-
else:
28-
self._default_settings()
22+
if not (username or apikey):
23+
settings = self._get_settings()
24+
username = settings.username
25+
apikey = settings.apikey
26+
27+
self.username = username
28+
self.apikey = apikey
2929

3030
self.project = project
3131

32-
def get_headers(self):
32+
def _get_settings(self):
33+
settings = Settings()
34+
settings.import_settings()
35+
return settings
36+
37+
def get_absolute_url(self, path):
38+
return urlparse.urljoin(self.API_ENDPOINT, path)
39+
40+
def get_headers(self, ctype):
3341
return {
34-
"Content-type": "application/json",
35-
"Accept": "application/json",
36-
"Authorization": base64.b64encode(
42+
'Content-type': 'application/{}'.format(ctype),
43+
'Accept': 'application/{}'.format(ctype),
44+
'Authorization': base64.b64encode(
3745
'{}:{}'.format(self.username, self.apikey)
3846
)
3947
}
4048

41-
def get_absolute_url(self, path):
42-
return self.API_ENDPOINT + path
49+
def get_data(self, raw_data, ctype):
50+
if ctype == self.CTYPE_XML:
51+
return xmltodict.unparse(raw_data)
4352

44-
def get(self, url):
45-
absolute_url = self.get_absolute_url(url)
46-
headers = self.get_headers()
47-
request = urllib2.Request(
48-
url=absolute_url,
49-
headers=headers,
50-
)
51-
logging.info('Making request to {} with headers {}'.format(
52-
request.get_full_url(),
53-
request.headers,
54-
))
55-
response = urllib2.urlopen(request)
56-
return self.handle_response(response)
53+
# Encodes the parameters by default (i.e. using json).
54+
return urllib.urlencode(raw_data)
5755

58-
def post(self, url, data):
59-
absolute_url = self.get_absolute_url(url)
60-
headers = self.get_headers()
61-
request = urllib2.Request(
62-
url=absolute_url,
63-
headers=headers,
64-
data=data,
65-
)
66-
response = urllib2.urlopen(request)
67-
return self.handle_response(response)
68-
69-
def handle_response(self, response):
56+
def _handle_response(self, response, ctype):
7057
try:
7158
status_code = response.getcode()
7259
content = response.read()
7360
logger.debug('{} returned status code {}'.format(
7461
response.url,
7562
status_code
7663
))
64+
65+
if ctype == self.CTYPE_XML:
66+
return xmltodict.parse(content)
7767
return json.loads(content)
7868
except Exception as e:
79-
logging.exception(e)
69+
logging.exception('%s: %s', e.__class__.__name__, e.message)
70+
71+
def _send_request(self, url, ctype=None, data=None):
72+
if not ctype:
73+
ctype = self.CTYPE_JSON
74+
75+
absolute_url = self.get_absolute_url(url)
76+
headers = self.get_headers(ctype)
77+
req_params = {
78+
'url': absolute_url,
79+
'headers': headers,
80+
}
81+
if data:
82+
data = self.get_data(data, ctype)
83+
req_params['data'] = data
84+
85+
request = urllib2.Request(**req_params)
86+
logging.info('Making request to {} with headers {}'.format(
87+
request.get_full_url(),
88+
request.headers,
89+
))
90+
91+
response = urllib2.urlopen(request)
92+
return self._handle_response(response, ctype)
93+
94+
def get(self, url, ctype=None):
95+
return self._send_request(url, ctype=ctype)
96+
97+
def post(self, url, data, ctype=None):
98+
return self._send_request(url, data=data, ctype=ctype)
8099

81100

82101
class CodeBaseAPI(Auth):
@@ -99,13 +118,42 @@ def types(self):
99118
def milestones(self):
100119
return self.get('/%s/milestones' % self.project)
101120

102-
def search(self, term):
103-
terms = term.split(':')
104-
if len(terms) == 1:
105-
escaped_term = urllib2.quote(terms[0])
106-
else:
107-
escaped_term = '{}:"{}"'.format(terms[0], urllib2.quote(terms[1]))
108-
return self.get('/%s/tickets?query=%s' % (self.project, escaped_term))
121+
def search(self, term=None, page=None, **kwargs):
122+
queries = []
123+
if term:
124+
queries.append(term.strip())
125+
126+
for term_name, term_value in kwargs.iteritems():
127+
if term_name.startswith('not_'):
128+
term_name = term_name.replace('not_', 'not-')
129+
queries.append('{}:"{}"'.format(term_name, term_value.strip()))
130+
131+
params = {'query': ' '.join(queries)}
132+
if page and page > 1 and type(page) is int:
133+
params['page'] = page
134+
135+
url = '/{}/tickets?{}'.format(self.project, urllib.urlencode(params))
136+
return self.get(url)
137+
138+
def search_all(self, term=None, **kwargs):
139+
page = 1
140+
tickets = []
141+
while True:
142+
try:
143+
tickets.extend(self.search(term=term, page=page, **kwargs))
144+
page += 1
145+
except urllib2.HTTPError:
146+
page -= 1
147+
break
148+
except Exception, e:
149+
logger.error(
150+
u'An error occured while searching for "%s" '
151+
u'(current page: %s):\n%s', term, page, e
152+
)
153+
page -= 1
154+
break
155+
156+
return tickets
109157

110158
def watchers(self, ticket_id):
111159
return self.get('/%s/tickets/%s/watchers' % (self.project, ticket_id))
@@ -137,6 +185,15 @@ def discussions(self):
137185
def discussion_categories(self):
138186
return self.get('/%s/discussions/categories' % self.project)
139187

188+
def create_ticket(self, data):
189+
# It looks like that only XML requests are accepted for creating new
190+
# tickets.
191+
return self.post(
192+
'/%s/tickets' % self.project,
193+
data,
194+
ctype=self.CTYPE_XML,
195+
)
196+
140197
def create_discussion(self, data):
141198
return self.post('/%s/discussions' % self.project, data)
142199

@@ -172,4 +229,4 @@ def hooks(self, repository):
172229
return self.get('/%s/%s/hooks' % (self.project, repository))
173230

174231
def add_hook(self, repository, data):
175-
return self.get('/%s/%s/hooks' % (self.project, repository), data)
232+
return self.post('/%s/%s/hooks' % (self.project, repository), data)

‎codebase/settings.py‎

100755100644
Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import errno
12
import json
2-
3-
from os.path import expanduser
3+
import os
44

55
from codebase import logger
66

@@ -17,19 +17,60 @@ class Settings(object):
1717
"""
1818
By default settings are expected to be in a file named `.codebase` in your home directory
1919
"""
20-
SETTINGS_FILE_PATH = expanduser("~") + '/.codebase'
20+
SETTINGS_FILE_PATH = '~/.codebase'
21+
22+
def __init__(self, file_path=None):
23+
# Initializing the credentials attributes.
24+
self.username = None
25+
self.apikey = None
26+
27+
# Getting the settings path.
28+
if not file_path or file_path.startswith('~'):
29+
file_path = file_path or self.SETTINGS_FILE_PATH
30+
try:
31+
file_path = os.path.expanduser(file_path)
32+
except ImportError:
33+
file_name = os.path.basename(file_path)
34+
file_path = os.path.join(os.getcwd(), file_name)
35+
logger.warning(
36+
u'Looks like you are running on GoogleAppEngine, which '
37+
u'means that "os.path.expanduser" is not supported. '
38+
u'Please override the settings logic in '
39+
u'`codebase.client.Auth._get_settings` or explicitely '
40+
u'pass the required credentials informations.'
41+
)
42+
logger.warning(
43+
u'The following path will be used in place of the default '
44+
u'value: {}'.format(file_path)
45+
)
2146

22-
def get_path(self):
23-
home = expanduser("~")
24-
return home + '.codebase'
47+
self.file_path = file_path
2548

26-
def __init__(self):
49+
def import_settings(self):
2750
try:
28-
with open(self.SETTINGS_FILE_PATH) as f:
29-
for k, v in json.loads(f.read()).iteritems():
30-
setattr(self, k, v)
31-
except IOError:
51+
with open(self.file_path, 'r') as f:
52+
settings = json.loads(f.read())
53+
54+
self.username = settings.pop('CODEBASE_USERNAME', None)
55+
self.apikey = settings.pop('CODEBASE_APIKEY', None)
56+
assert self.username is not None, '"CODEBASE_USERNAME" is required'
57+
assert self.apikey is not None, '"CODEBASE_APIKEY" is required'
58+
59+
for k, v in settings.iteritems():
60+
setattr(self, k, v)
61+
except IOError, io_err:
3262
logger.error(
33-
'Settings file "~/.codebase" not found. Please add your settings in JSON format e.g. %s'
34-
% EXAMPLE_FORMAT
63+
u'Settings file "{}" not found. '
64+
u'Please add your settings in JSON format e.g. {}'.format(
65+
self.file_path,
66+
EXAMPLE_FORMAT,
67+
)
3568
)
69+
if io_err.errno == errno.EACCES:
70+
# EACCES = 13, which means "file not accessible" (permission
71+
# denied).
72+
logger.error(
73+
'The problem could be caused by the name of the file. '
74+
'Try to use a non-hidden file name (i.e. a file which '
75+
'does not start with a dot).'
76+
)

‎codebase/tests.py‎

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)