Skip to content

Commit f9eb9f0

Browse files
author
Rachel Sanders
committed
Merge pull request #18 from linkedin/requests
Changed out the connection logic to use requests instead of urllib2 - hooray
2 parents bab0a62 + 03b03a7 commit f9eb9f0

9 files changed

+34
-120
lines changed

pyexchange/connection.py

+24-83
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,10 @@
44
55
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
66
"""
7-
import logging
8-
import sys
9-
from ntlm import HTTPNtlmAuthHandler
10-
11-
try:
12-
import urllib2
13-
except ImportError: # Python 3
14-
import urllib.request as urllib2
7+
import requests
8+
from requests_ntlm import HttpNtlmAuth
159

16-
try:
17-
from httplib import HTTPException
18-
except ImportError: # Python 3
19-
from http.client import HTTPException
10+
import logging
2011

2112
from .exceptions import FailedExchangeException
2213

@@ -39,7 +30,7 @@ def __init__(self, url, username, password, **kwargs):
3930
self.password = password
4031

4132
self.handler = None
42-
self.opener = None
33+
self.session = None
4334
self.password_manager = None
4435

4536
def build_password_manager(self):
@@ -48,85 +39,35 @@ def build_password_manager(self):
4839

4940
log.debug(u'Constructing password manager')
5041

51-
self.password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
52-
self.password_manager.add_password(None, self.url, self.username, self.password)
42+
self.password_manager = HttpNtlmAuth(self.username, self.password)
5343

5444
return self.password_manager
5545

56-
def build_handler(self):
57-
if self.handler:
58-
return self.handler
46+
def build_session(self):
47+
if self.session:
48+
return self.session
5949

60-
log.debug(u'Constructing handler')
50+
log.debug(u'Constructing opener')
6151

6252
self.password_manager = self.build_password_manager()
63-
self.handler = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(self.password_manager)
6453

65-
return self.handler
54+
self.session = requests.Session()
55+
self.session.auth = self.password_manager
6656

67-
def build_opener(self):
68-
if self.opener:
69-
return self.opener
57+
return self.session
7058

71-
log.debug(u'Constructing opener')
59+
def send(self, body, headers=None, retries=2, timeout=30, encoding=u"utf-8"):
60+
if not self.session:
61+
self.session = self.build_session()
7262

73-
self.handler = self.build_handler()
74-
self.opener = urllib2.build_opener(self.handler)
63+
try:
64+
response = self.session.post(self.url, data=body, headers=headers)
65+
response.raise_for_status()
66+
except requests.exceptions.RequestException as err:
67+
raise FailedExchangeException(u'Unable to connect to Exchange: %s' % err)
7568

76-
return self.opener
69+
log.info(u'Got response: {code}'.format(code=response.status_code))
70+
log.debug(u'Got response headers: {headers}'.format(headers=response.headers))
71+
log.debug(u'Got body: {body}'.format(body=response.text))
7772

78-
def send(self, body, headers=None, retries=2, timeout=30, encoding=u"utf-8"):
79-
if not self.opener:
80-
self.opener = self.build_opener()
81-
82-
url = self.url
83-
84-
# lxml tostring returns str in Python 2, and bytes in python 3
85-
# if XML is actually unicode, urllib2 will barf.
86-
# Oddly enough this only seems to be a problem in 2.7. 2.6 doesn't seem to care.
87-
# The url used should be a bytestring as well. 2.6 doesn't care about this but 2.7 does.
88-
if sys.version_info < (3, 0):
89-
if isinstance(body, unicode):
90-
body = body.encode(encoding)
91-
if isinstance(url, unicode):
92-
url = url.encode(encoding)
93-
else:
94-
if isinstance(body, str):
95-
body = body.encode(encoding)
96-
if isinstance(url, str):
97-
url = url.encode(encoding)
98-
99-
request = urllib2.Request(url, body)
100-
101-
if headers:
102-
for header in headers:
103-
log.debug(u'Adding header: {name} - {value}'.format(name=header[0], value=header[1]))
104-
request.add_header(header[0], header[1])
105-
106-
error = None
107-
for retry in range(retries + 1):
108-
log.info(u'Connection attempt #{0} of {1}'.format(retry + 1, retries))
109-
try:
110-
# retrieve the result
111-
log.info(u'Sending request to url: {0}'.format(request.get_full_url()))
112-
try:
113-
response = self.opener.open(request, timeout=timeout)
114-
115-
except urllib2.HTTPError as err:
116-
# Called for 500 errors
117-
raise FailedExchangeException(u'Unable to connect to Exchange: %s' % err)
118-
119-
response_code = response.getcode()
120-
body = response.read().decode(encoding)
121-
122-
log.info(u'Got response: {code}'.format(code=response_code))
123-
log.debug(u'Got response headers: {headers}'.format(headers=response.info()))
124-
log.debug(u'Got body: {body}'.format(body=body))
125-
126-
return body
127-
except HTTPException as err:
128-
log.error(u'Caught err, retrying: {err}'.format(err=err))
129-
error = err
130-
131-
# All retries used up, re-throw the exception.
132-
raise error
73+
return response.text

pyexchange/exchange2010/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ def folder(self):
3737
return Exchange2010FolderService(service=self)
3838

3939
def _send_soap_request(self, body, headers=None, retries=2, timeout=30, encoding="utf-8"):
40-
headers = [("Accept", "text/xml"), ("Content-type", "text/xml; charset=%s " % encoding)]
40+
headers = {
41+
"Accept" : "text/xml",
42+
"Content-type" : "text/xml; charset=%s " % encoding
43+
}
4144
return super(Exchange2010Service, self)._send_soap_request(body, headers=headers, retries=retries, timeout=timeout, encoding=encoding)
4245

4346
def _check_for_errors(self, xml_tree):

requirements.txt

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
lxml
2-
python-ntlm
3-
pytz
1+
lxml==3.4.0
2+
pytz==2014.7
3+
requests==2.4.1
4+
requests-ntlm==0.0.3

tests/__init__.py

-27
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,5 @@
44
55
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
66
"""
7-
from functools import wraps
8-
from nose.plugins.attrib import attr
9-
from nose.plugins.skip import SkipTest
107

11-
"""
12-
Got this from:
13-
14-
http://www.natpryce.com/articles/000788.html
15-
https://gist.github.com/997195
16-
17-
"""
18-
19-
20-
def fail(message):
21-
raise AssertionError(message)
22-
23-
def wip(f):
24-
"""
25-
Use this as a decorator to mark tests that are "works in progress"
26-
"""
27-
@wraps(f)
28-
def run_test(*args, **kwargs):
29-
try:
30-
f(*args, **kwargs)
31-
except Exception as e:
32-
raise SkipTest("WIP test failed: " + str(e))
33-
fail("test passed but marked as work in progress")
348

35-
return attr('wip')(run_test)

tests/exchange2010/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,3 @@
44
55
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
66
"""
7-
__author__ = 'rsanders'

tests/exchange2010/test_create_event.py

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from pyexchange.exceptions import *
1515

1616
from .fixtures import *
17-
from .. import wip
1817

1918
class Test_PopulatingANewEvent(unittest.TestCase):
2019
""" Tests all the attribute setting works when creating a new event """

tests/exchange2010/test_delete_event.py

-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
from pyexchange.connection import ExchangeNTLMAuthConnection
1212

1313
from .fixtures import *
14-
from .. import wip
15-
1614

1715
class Test_EventDeletion(unittest.TestCase):
1816
event = None

tests/exchange2010/test_event_actions.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from pyexchange.exceptions import *
1313

1414
from .fixtures import *
15-
from .. import wip
15+
1616

1717
class Test_EventActions(unittest.TestCase):
1818
event = None

tests/fixtures.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
Unless required by applicable law or agreed to in writing, software?distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
66
"""
77
FAKE_EXCHANGE_URL = u'http://10.0.0.0/nothing'
8-
FAKE_EXCHANGE_USERNAME = u'nobody'
8+
FAKE_EXCHANGE_USERNAME = u'FAKEDOMAIN\\nobody'
99
FAKE_EXCHANGE_PASSWORD = u'totallyfake'

0 commit comments

Comments
 (0)