Skip to content

Commit 2f9a8a3

Browse files
author
James Socol
committed
Python 2 and 3 compatibility
- Drop PyCURL, since it's not Py3k compatible. - Drop bitly_http module, just inline urllib(2) code. - Steal *_types lists from six. - Update isinstance calls. - Update excepts. - Decode response body as utf-8.
1 parent dcf5e28 commit 2f9a8a3

6 files changed

Lines changed: 85 additions & 127 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
build
22
dist
33
bitly_api.egg-info
4-
*.pyc
4+
*.pyc
5+
*.swp

.travis.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
language: python
22
python:
3-
- 2.6
4-
- 2.7
3+
- "2.6"
4+
- "2.7"
5+
- "3.3"
56
env:
67
global:
78
secure: mpFkhLxiDEpGeAMfcSx601ZFykkrKilpWch6qsx62qNhZTVrf33bru9hFooi/NpGGx+2A+epRj705BibAG8OUvjJBJCAZnrNbrn/j9XsBwA4bmsoDzLg+7eaElneX3J+MWlT4419nVhnH+k3W1yRX+0HrsmE+5mGmxuIE4DdIpI=
89
install: pip install flake8
9-
script:
10-
- ./run_tests.sh
11-
- flake8 bitly_api
10+
script: ./run_tests.sh

bitly_api/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from bitly_api import Connection, BitlyError, Error
1+
from __future__ import absolute_import
2+
from bitly_api.bitly_api import Connection, BitlyError, Error
23
__version__ = '0.3'
34
__author__ = "Jehiah Czebotar <[email protected]>"
45
__all__ = ["Connection", "BitlyError", "Error"]

bitly_api/bitly_api.py

Lines changed: 69 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
1-
import bitly_http
21
import hashlib
3-
try:
4-
import json
5-
except ImportError:
6-
import simplejson as json
2+
import json
73
import sys
84
import time
95
import types
10-
import urllib
116
import warnings
127

8+
try:
9+
from urllib.request import build_opener, HTTPRedirectHandler
10+
from urllib.parse import urlencode
11+
from urllib.error import URLError, HTTPError
12+
string_types = str,
13+
integer_types = int,
14+
numeric_types = (int, float)
15+
text_type = str
16+
binary_type = bytes
17+
except ImportError as e:
18+
from urllib2 import build_opener, HTTPRedirectHandler, URLError, HTTPError
19+
from urllib import urlencode
20+
string_types = basestring,
21+
integer_types = (int, long)
22+
numeric_types = (int, long, float)
23+
text_type = unicode
24+
binary_type = str
25+
26+
27+
class DontRedirect(HTTPRedirectHandler):
28+
def redirect_response(self, req, fp, code, msg, headers, newurl):
29+
if code in (301, 302, 303, 307):
30+
raise HTTPError(req.get_full_url(), code, msg, headers, fp)
31+
1332

1433
class Error(Exception):
1534
pass
@@ -22,9 +41,9 @@ def __init__(self, code, message):
2241

2342

2443
def _utf8(s):
25-
if isinstance(s, unicode):
44+
if isinstance(s, text_type):
2645
s = s.encode('utf-8')
27-
assert isinstance(s, str)
46+
assert isinstance(s, binary_type)
2847
return s
2948

3049

@@ -35,7 +54,7 @@ def _utf8_params(params):
3554
for k, v in params.items():
3655
if v is None:
3756
continue
38-
if isinstance(v, (int, long, float)):
57+
if isinstance(v, numeric_types):
3958
v = str(v)
4059
if isinstance(v, (list, tuple)):
4160
v = [_utf8(x) for x in v]
@@ -314,26 +333,26 @@ def user_link_history(self, created_before=None, created_after=None,
314333
private=None):
315334
params = dict()
316335
if created_before is not None:
317-
assert isinstance(limit, int)
336+
assert isinstance(limit, integer_types)
318337
params["created_before"] = created_before
319338
if created_after is not None:
320-
assert isinstance(limit, int)
339+
assert isinstance(limit, integer_types)
321340
params["created_after"] = created_after
322341
if archived is not None:
323-
assert isinstance(archived, str)
342+
assert isinstance(archived, string_types)
324343
archived = archived.lower()
325344
assert archived is "on" or "off" or "both"
326345
params["archived"] = archived
327346
if private is not None:
328-
assert isinstance(private, str)
347+
assert isinstance(private, string_types)
329348
private = private.lower()
330349
assert private is "on" or "off" or "both"
331350
params["private"] = private
332351
if limit is not None:
333-
assert isinstance(limit, int)
352+
assert isinstance(limit, integer_types)
334353
params["limit"] = str(limit)
335354
if offset is not None:
336-
assert isinstance(offset, int)
355+
assert isinstance(offset, integer_types)
337356
params["offset"] = str(offset)
338357
data = self._call_oauth2("v3/user/link_history", params)
339358
return data["link_history"]
@@ -346,10 +365,10 @@ def user_network_history(self, offset=None, expand_client_id=False,
346365
if expand_user is True:
347366
params["expand_user"] = "true"
348367
if offset is not None:
349-
assert isinstance(offset, int)
368+
assert isinstance(offset, integer_types)
350369
params["offset"] = str(offset)
351370
if limit is not None:
352-
assert isinstance(limit, int)
371+
assert isinstance(limit, integer_types)
353372
params["limit"] = str(limit)
354373
data = self._call_oauth2("v3/user/network_history", params)
355374
return data
@@ -508,10 +527,10 @@ def bundle_create(self, private=False, title=None, description=None):
508527
if private:
509528
params["private"] = "true"
510529
if title is not None:
511-
assert isinstance(title, str)
530+
assert isinstance(title, string_types)
512531
params["title"] = title
513532
if description is not None:
514-
assert isinstance(description, str)
533+
assert isinstance(description, string_types)
515534
params["description"] = description
516535
data = self._call_oauth2_metrics("v3/bundle/create", params)
517536
return data
@@ -521,13 +540,13 @@ def bundle_edit(self, bundle_link, edit=None, title=None, description=None,
521540
"""edit a bundle for the authenticated user"""
522541
params = dict(bundle_link=bundle_link)
523542
if edit:
524-
assert isinstance(edit, str)
543+
assert isinstance(edit, string_types)
525544
params["edit"] = edit
526545
if title:
527-
assert isinstance(title, str)
546+
assert isinstance(title, string_types)
528547
params["title"] = title
529548
if description:
530-
assert isinstance(description, str)
549+
assert isinstance(description, string_types)
531550
params["description"] = description
532551
if private is not None:
533552
if private:
@@ -540,7 +559,7 @@ def bundle_edit(self, bundle_link, edit=None, title=None, description=None,
540559
else:
541560
params["preview"] = "false"
542561
if og_image:
543-
assert isinstance(og_image, str)
562+
assert isinstance(og_image, string_types)
544563
params["og_image"] = og_image
545564
data = self._call_oauth2_metrics("v3/bundle/edit", params)
546565
return data
@@ -549,7 +568,7 @@ def bundle_link_add(self, bundle_link, link, title=None):
549568
"""add a link to a bundle"""
550569
params = dict(bundle_link=bundle_link, link=link)
551570
if title:
552-
assert isinstance(title, str)
571+
assert isinstance(title, string_types)
553572
params["title"] = title
554573
data = self._call_oauth2_metrics("v3/bundle/link_add", params)
555574
return data
@@ -581,7 +600,7 @@ def bundle_link_edit(self, bundle_link, link, edit, title=None,
581600
params = dict(bundle_link=bundle_link, link=link)
582601
if edit == "title":
583602
params["edit"] = edit
584-
assert isinstance(title, str)
603+
assert isinstance(title, string_types)
585604
params["title"] = title
586605
elif edit == "preview":
587606
params["edit"] = edit
@@ -681,16 +700,16 @@ def search(self, query, offset=None, cities=None, domain=None, fields=None,
681700
limit=10, lang='en'):
682701
params = dict(query=query, lang=lang)
683702
if offset:
684-
assert isinstance(offset, int)
703+
assert isinstance(offset, integer_types)
685704
params["offset"] = str(offset)
686705
if cities: # TODO: check format
687-
assert isinstance(cities, str)
706+
assert isinstance(cities, string_types)
688707
params["cities"] = cities
689708
if domain:
690-
assert isinstance(domain, str)
709+
assert isinstance(domain, string_types)
691710
params["domain"] = domain
692711
if fields:
693-
assert isinstance(fields, str)
712+
assert isinstance(fields, string_types)
694713
params["fields"] = fields
695714
data = self._call_oauth2_metrics("v3/search", params, limit=limit)
696715
return data['results']
@@ -723,26 +742,27 @@ def _call_oauth2_metrics(self, endpoint, params, unit=None, units=None,
723742
assert unit in ("minute", "hour", "day", "week", "mweek", "month")
724743
params["unit"] = unit
725744
if units is not None:
726-
assert isinstance(units, int), "Unit (%r) must be integer" % units
745+
assert isinstance(units, integer_types), \
746+
"Unit (%r) must be integer" % units
727747
params["units"] = units
728748
if tz_offset is not None:
729749
# tz_offset can either be a hour offset, or a timezone like
730750
# North_America/New_York
731-
if isinstance(tz_offset, int):
751+
if isinstance(tz_offset, integer_types):
732752
msg = "integer tz_offset must be between -12 and 12"
733753
assert -12 <= tz_offset <= 12, msg
734754
else:
735-
assert isinstance(tz_offset, (str, unicode))
755+
assert isinstance(tz_offset, string_types)
736756
params["tz_offset"] = tz_offset
737757
if rollup is not None:
738758
assert isinstance(rollup, bool)
739759
params["rollup"] = "true" if rollup else "false"
740760
if limit is not None:
741-
assert isinstance(limit, int)
761+
assert isinstance(limit, integer_types)
742762
params["limit"] = limit
743763
if unit_reference_ts is not None:
744764
assert (unit_reference_ts == 'now' or
745-
isinstance(unit_reference_ts, (int, long)))
765+
isinstance(unit_reference_ts, integer_types))
746766
params["unit_reference_ts"] = unit_reference_ts
747767

748768
return self._call_oauth2(endpoint, params)
@@ -773,21 +793,28 @@ def _call(self, host, method, params, secret=None, timeout=5000):
773793
'scheme': scheme,
774794
'host': host,
775795
'method': method,
776-
'params': urllib.urlencode(params, doseq=1)
796+
'params': urlencode(params, doseq=1)
777797
}
778798

779799
try:
780-
http_response = bitly_http.get(request, timeout,
781-
user_agent=self.user_agent)
782-
if http_response['http_status_code'] != 200:
783-
raise BitlyError(500, http_response['result'])
784-
if not http_response['result'].startswith('{'):
785-
raise BitlyError(500, http_response['result'])
786-
data = json.loads(http_response['result'])
800+
opener = build_opener(DontRedirect())
801+
opener.addheaders = [('User-agent', self.user_agent + ' urllib')]
802+
response = opener.open(request)
803+
code = response.code
804+
result = response.read().decode('utf-8')
805+
if code != 200:
806+
raise BitlyError(500, result)
807+
if not result.startswith('{'):
808+
raise BitlyError(500, result)
809+
data = json.loads(result)
787810
if data.get('status_code', 500) != 200:
788811
raise BitlyError(data.get('status_code', 500),
789812
data.get('status_txt', 'UNKNOWN_ERROR'))
790813
return data
814+
except URLError as e:
815+
raise BitlyError(500, str(e))
816+
except HTTPError as e:
817+
raise BitlyError(e.code, e.read())
791818
except BitlyError:
792819
raise
793820
except Exception:

bitly_api/bitly_http.py

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

test/test_bitly_api.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,12 @@ def testProDomain():
6060
try:
6161
result = bitly.pro_domain(domain)
6262
assert result == test_data[domain], domain
63-
except bitly_api.BitlyError, e:
63+
except bitly_api.BitlyError as e:
6464
assert str(e) == test_data[domain]
65+
66+
67+
def testUserInfo():
68+
bitly = get_connection()
69+
data = bitly.user_info()
70+
assert data is not None
71+
assert 'login' in data

0 commit comments

Comments
 (0)