├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── bitly_api ├── __init__.py └── bitly_api.py ├── run_tests.sh ├── setup.py └── test └── test_bitly_api.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | bitly_api.egg-info 4 | *.pyc 5 | *.swp 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | env: 7 | global: 8 | secure: mpFkhLxiDEpGeAMfcSx601ZFykkrKilpWch6qsx62qNhZTVrf33bru9hFooi/NpGGx+2A+epRj705BibAG8OUvjJBJCAZnrNbrn/j9XsBwA4bmsoDzLg+7eaElneX3J+MWlT4419nVhnH+k3W1yRX+0HrsmE+5mGmxuIE4DdIpI= 9 | install: pip install flake8 10 | script: ./run_tests.sh 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Bitly Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## API Documentation 2 | 3 | For documentation of current version of the Bitly API visit https://dev.bitly.com/ 4 | -------------------------------------------------------------------------------- /bitly_api/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from bitly_api.bitly_api import Connection, BitlyError, Error 3 | __version__ = '0.3' 4 | __author__ = "Jehiah Czebotar " 5 | __all__ = ["Connection", "BitlyError", "Error"] 6 | __doc__ = """ 7 | This is a python library for the bitly api 8 | 9 | all methods raise BitlyError on an unexpected response, or a problem with input 10 | format 11 | """ 12 | -------------------------------------------------------------------------------- /bitly_api/bitly_api.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import sys 4 | import time 5 | import types 6 | import warnings 7 | 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 | 32 | 33 | class Error(Exception): 34 | pass 35 | 36 | 37 | class BitlyError(Error): 38 | def __init__(self, code, message): 39 | Error.__init__(self, message) 40 | self.code = code 41 | 42 | 43 | def _utf8(s): 44 | if isinstance(s, text_type): 45 | s = s.encode('utf-8') 46 | assert isinstance(s, binary_type) 47 | return s 48 | 49 | 50 | def _utf8_params(params): 51 | """encode a dictionary of URL parameters (including iterables) as utf-8""" 52 | assert isinstance(params, dict) 53 | encoded_params = [] 54 | for k, v in params.items(): 55 | if v is None: 56 | continue 57 | if isinstance(v, numeric_types): 58 | v = str(v) 59 | if isinstance(v, (list, tuple)): 60 | v = [_utf8(x) for x in v] 61 | else: 62 | v = _utf8(v) 63 | encoded_params.append((k, v)) 64 | return dict(encoded_params) 65 | 66 | 67 | class Connection(object): 68 | """ 69 | This is a python library for accessing the bitly api 70 | http://github.com/bitly/bitly-api-python 71 | 72 | Usage: 73 | import bitly_api 74 | c = bitly_api.Connection('bitlyapidemo','R_{{apikey}}') 75 | # or to use oauth2 endpoints 76 | c = bitly_api.Connection(access_token='...') 77 | c.shorten('http://www.google.com/') 78 | """ 79 | 80 | def __init__(self, login=None, api_key=None, access_token=None, 81 | secret=None): 82 | self.host = 'api.bit.ly' 83 | self.ssl_host = 'api-ssl.bit.ly' 84 | self.login = login 85 | self.api_key = api_key 86 | self.access_token = access_token 87 | self.secret = secret 88 | (major, minor, micro, releaselevel, serial) = sys.version_info 89 | parts = (major, minor, micro, '?') 90 | self.user_agent = "Python/%d.%d.%d bitly_api/%s" % parts 91 | 92 | def shorten(self, uri, x_login=None, x_apiKey=None, preferred_domain=None): 93 | """ creates a bitly link for a given long url 94 | @parameter uri: long url to shorten 95 | @parameter x_login: login of a user to shorten on behalf of 96 | @parameter x_apiKey: apiKey of a user to shorten on behalf of 97 | @parameter preferred_domain: bit.ly[default], bitly.com, or j.mp 98 | """ 99 | params = dict(uri=uri) 100 | if preferred_domain: 101 | params['domain'] = preferred_domain 102 | if x_login: 103 | params.update({ 104 | 'x_login': x_login, 105 | 'x_apiKey': x_apiKey}) 106 | data = self._call(self.host, 'v3/shorten', params, self.secret) 107 | return data['data'] 108 | 109 | def expand(self, hash=None, shortUrl=None, link=None): 110 | """ given a bitly url or hash, decode it and return the target url 111 | @parameter hash: one or more bitly hashes 112 | @parameter shortUrl: one or more bitly short urls 113 | @parameter link: one or more bitly short urls (preferred vocabulary) 114 | """ 115 | if link and not shortUrl: 116 | shortUrl = link 117 | 118 | if not hash and not shortUrl: 119 | raise BitlyError(500, 'MISSING_ARG_SHORTURL') 120 | params = dict() 121 | if hash: 122 | params['hash'] = hash 123 | if shortUrl: 124 | params['shortUrl'] = shortUrl 125 | 126 | data = self._call(self.host, 'v3/expand', params, self.secret) 127 | return data['data']['expand'] 128 | 129 | def clicks(self, hash=None, shortUrl=None): 130 | """ 131 | given a bitly url or hash, get statistics about the clicks on that link 132 | """ 133 | warnings.warn("/v3/clicks is depricated in favor of /v3/link/clicks", 134 | DeprecationWarning) 135 | if not hash and not shortUrl: 136 | raise BitlyError(500, 'MISSING_ARG_SHORTURL') 137 | params = dict() 138 | if hash: 139 | params['hash'] = hash 140 | if shortUrl: 141 | params['shortUrl'] = shortUrl 142 | 143 | data = self._call(self.host, 'v3/clicks', params, self.secret) 144 | return data['data']['clicks'] 145 | 146 | def referrers(self, hash=None, shortUrl=None): 147 | """ 148 | given a bitly url or hash, get statistics about the referrers of that 149 | link 150 | """ 151 | warnings.warn("/v3/referrers is depricated in favor of " 152 | "/v3/link/referrers", DeprecationWarning) 153 | if not hash and not shortUrl: 154 | raise BitlyError(500, 'MISSING_ARG_SHORTURL') 155 | params = dict() 156 | if hash: 157 | params['hash'] = hash 158 | if shortUrl: 159 | params['shortUrl'] = shortUrl 160 | 161 | data = self._call(self.host, 'v3/referrers', params, self.secret) 162 | return data['data']['referrers'] 163 | 164 | def clicks_by_day(self, hash=None, shortUrl=None): 165 | """ given a bitly url or hash, get a time series of clicks 166 | per day for the last 30 days in reverse chronological order 167 | (most recent to least recent) """ 168 | warnings.warn("/v3/clicks_by_day is depricated in favor of " 169 | "/v3/link/clicks?unit=day", DeprecationWarning) 170 | if not hash and not shortUrl: 171 | raise BitlyError(500, 'MISSING_ARG_SHORTURL') 172 | params = dict() 173 | if hash: 174 | params['hash'] = hash 175 | if shortUrl: 176 | params['shortUrl'] = shortUrl 177 | 178 | data = self._call(self.host, 'v3/clicks_by_day', params, self.secret) 179 | return data['data']['clicks_by_day'] 180 | 181 | def clicks_by_minute(self, hash=None, shortUrl=None): 182 | """ given a bitly url or hash, get a time series of clicks 183 | per minute for the last 30 minutes in reverse chronological 184 | order (most recent to least recent)""" 185 | warnings.warn("/v3/clicks_by_minute is depricated in favor of " 186 | "/v3/link/clicks?unit=minute", DeprecationWarning) 187 | if not hash and not shortUrl: 188 | raise BitlyError(500, 'MISSING_ARG_SHORTURL') 189 | params = dict() 190 | if hash: 191 | params['hash'] = hash 192 | if shortUrl: 193 | params['shortUrl'] = shortUrl 194 | 195 | data = self._call(self.host, 'v3/clicks_by_minute', params, 196 | self.secret) 197 | return data['data']['clicks_by_minute'] 198 | 199 | def link_clicks(self, link, **kwargs): 200 | params = dict(link=link) 201 | data = self._call_oauth2_metrics("v3/link/clicks", params, **kwargs) 202 | return data["link_clicks"] 203 | 204 | def link_encoders(self, link, **kwargs): 205 | """return the bitly encoders who have saved this link""" 206 | params = dict(link=link) 207 | data = self._call(self.host, 'v3/link/encoders', params, **kwargs) 208 | return data['data'] 209 | 210 | def link_encoders_count(self, link, **kwargs): 211 | """return the count of bitly encoders who have saved this link""" 212 | params = dict(link=link) 213 | data = self._call(self.host, 'v3/link/encoders_count', params, 214 | **kwargs) 215 | return data['data'] 216 | 217 | def link_referring_domains(self, link, **kwargs): 218 | """ 219 | returns the domains that are referring traffic to a single bitly link 220 | """ 221 | params = dict(link=link) 222 | data = self._call_oauth2_metrics("v3/link/referring_domains", params, 223 | **kwargs) 224 | return data["referring_domains"] 225 | 226 | def link_referrers_by_domain(self, link, **kwargs): 227 | """ 228 | returns the pages that are referring traffic to a single bitly link, 229 | grouped by domain 230 | """ 231 | params = dict(link=link) 232 | data = self._call_oauth2_metrics("v3/link/referrers_by_domain", params, 233 | **kwargs) 234 | return data["referrers"] 235 | 236 | def link_referrers(self, link, **kwargs): 237 | """ 238 | returns the pages are are referring traffic to a single bitly link 239 | """ 240 | params = dict(link=link) 241 | data = self._call_oauth2_metrics("v3/link/referrers", params, **kwargs) 242 | return data["referrers"] 243 | 244 | def link_shares(self, link, **kwargs): 245 | """return number of shares of a bitly link""" 246 | params = dict(link=link) 247 | data = self._call_oauth2_metrics("v3/link/shares", params, **kwargs) 248 | return data 249 | 250 | def link_countries(self, link, **kwargs): 251 | params = dict(link=link) 252 | data = self._call_oauth2_metrics("v3/link/countries", params, **kwargs) 253 | return data["countries"] 254 | 255 | def user_clicks(self, **kwargs): 256 | """aggregate number of clicks on all of this user's bitly links""" 257 | data = self._call_oauth2_metrics('v3/user/clicks', dict(), **kwargs) 258 | return data 259 | 260 | def user_countries(self, **kwargs): 261 | """ 262 | aggregate metrics about countries from which people are clicking on all 263 | of a user's bitly links 264 | """ 265 | data = self._call_oauth2_metrics('v3/user/countries', dict(), **kwargs) 266 | return data["countries"] 267 | 268 | def user_popular_links(self, **kwargs): 269 | data = self._call_oauth2_metrics("v3/user/popular_links", dict(), 270 | **kwargs) 271 | return data["popular_links"] 272 | 273 | def user_referrers(self, **kwargs): 274 | """ 275 | aggregate metrics about the referrers for all of the authed user's 276 | bitly links 277 | """ 278 | data = self._call_oauth2_metrics("v3/user/referrers", dict(), **kwargs) 279 | return data["referrers"] 280 | 281 | def user_referring_domains(self, **kwargs): 282 | """ 283 | aggregate metrics about the domains referring traffic to all of the 284 | authed user's bitly links 285 | """ 286 | data = self._call_oauth2_metrics("v3/user/referring_domains", dict(), 287 | **kwargs) 288 | return data["referring_domains"] 289 | 290 | def user_share_counts(self, **kwargs): 291 | """number of shares by authed user in given time period""" 292 | data = self._call_oauth2_metrics("v3/user/share_counts", dict(), 293 | **kwargs) 294 | return data["share_counts"] 295 | 296 | def user_share_counts_by_share_type(self, **kwargs): 297 | """ 298 | number of shares by authed user broken down by type (facebook, twitter, 299 | email) in a give time period 300 | """ 301 | data = self._call_oauth2_metrics("v3/user/share_counts_by_share_type", 302 | dict(), **kwargs) 303 | return data["share_counts_by_share_type"] 304 | 305 | def user_shorten_counts(self, **kwargs): 306 | data = self._call_oauth2_metrics("v3/user/shorten_counts", dict(), 307 | **kwargs) 308 | return data["user_shorten_counts"] 309 | 310 | def user_tracking_domain_list(self): 311 | data = self._call_oauth2("v3/user/tracking_domain_list", dict()) 312 | return data["tracking_domains"] 313 | 314 | def user_tracking_domain_clicks(self, domain, **kwargs): 315 | params = dict(domain=domain) 316 | data = self._call_oauth2_metrics("v3/user/tracking_domain_clicks", 317 | params, **kwargs) 318 | return data["tracking_domain_clicks"] 319 | 320 | def user_tracking_domain_shorten_counts(self, domain, **kwargs): 321 | params = dict(domain=domain) 322 | data = self._call_oauth2_metrics( 323 | "v3/user/tracking_domain_shorten_counts", params, **kwargs) 324 | return data["tracking_domain_shorten_counts"] 325 | 326 | def user_info(self, **kwargs): 327 | """return or update info about a user""" 328 | data = self._call_oauth2("v3/user/info", kwargs) 329 | return data 330 | 331 | def user_link_history(self, created_before=None, created_after=None, 332 | archived=None, limit=None, offset=None, 333 | private=None): 334 | params = dict() 335 | if created_before is not None: 336 | assert isinstance(limit, integer_types) 337 | params["created_before"] = created_before 338 | if created_after is not None: 339 | assert isinstance(limit, integer_types) 340 | params["created_after"] = created_after 341 | if archived is not None: 342 | assert isinstance(archived, string_types) 343 | archived = archived.lower() 344 | assert archived is "on" or "off" or "both" 345 | params["archived"] = archived 346 | if private is not None: 347 | assert isinstance(private, string_types) 348 | private = private.lower() 349 | assert private is "on" or "off" or "both" 350 | params["private"] = private 351 | if limit is not None: 352 | assert isinstance(limit, integer_types) 353 | params["limit"] = str(limit) 354 | if offset is not None: 355 | assert isinstance(offset, integer_types) 356 | params["offset"] = str(offset) 357 | data = self._call_oauth2("v3/user/link_history", params) 358 | return data["link_history"] 359 | 360 | def user_network_history(self, offset=None, expand_client_id=False, 361 | limit=None, expand_user=False): 362 | params = dict() 363 | if expand_client_id is True: 364 | params["expand_client_id"] = "true" 365 | if expand_user is True: 366 | params["expand_user"] = "true" 367 | if offset is not None: 368 | assert isinstance(offset, integer_types) 369 | params["offset"] = str(offset) 370 | if limit is not None: 371 | assert isinstance(limit, integer_types) 372 | params["limit"] = str(limit) 373 | data = self._call_oauth2("v3/user/network_history", params) 374 | return data 375 | 376 | def info(self, hash=None, shortUrl=None, link=None): 377 | """ return the page title for a given bitly link """ 378 | if link and not shortUrl: 379 | shortUrl = link 380 | 381 | if not hash and not shortUrl: 382 | raise BitlyError(500, 'MISSING_ARG_SHORTURL') 383 | params = dict() 384 | if hash: 385 | params['hash'] = hash 386 | if shortUrl: 387 | params['shortUrl'] = shortUrl 388 | 389 | data = self._call(self.host, 'v3/info', params, self.secret) 390 | return data['data']['info'] 391 | 392 | def link_lookup(self, url): 393 | """query for a bitly link based on a long url (or list of long urls)""" 394 | params = dict(url=url) 395 | data = self._call(self.host, 'v3/link/lookup', params, self.secret) 396 | return data['data']['link_lookup'] 397 | 398 | def lookup(self, url): 399 | """ query for a bitly link based on a long url """ 400 | warnings.warn("/v3/lookup is depricated in favor of /v3/link/lookup", 401 | DeprecationWarning) 402 | 403 | params = dict(url=url) 404 | 405 | data = self._call(self.host, 'v3/lookup', params, self.secret) 406 | return data['data']['lookup'] 407 | 408 | def user_link_edit(self, link, edit, title=None, note=None, private=None, 409 | user_ts=None, archived=None): 410 | """edit a link in a user's history""" 411 | params = dict() 412 | 413 | if not link: 414 | raise BitlyError(500, 'MISSING_ARG_LINK') 415 | 416 | if not edit: 417 | raise BitlyError(500, 'MISSING_ARG_EDIT') 418 | 419 | params['link'] = link 420 | params['edit'] = edit 421 | if title is not None: 422 | params['title'] = str(title) 423 | if note is not None: 424 | params['note'] = str(note) 425 | if private is not None: 426 | params['private'] = bool(private) 427 | if user_ts is not None: 428 | params['user_ts'] = user_ts 429 | if archived is not None: 430 | params['archived'] = archived 431 | 432 | data = self._call_oauth2("v3/user/link_edit", params) 433 | return data['link_edit'] 434 | 435 | def user_link_lookup(self, url): 436 | """ 437 | query for whether a user has shortened a particular long URL. don't 438 | confuse with v3/link/lookup. 439 | """ 440 | params = dict(url=url) 441 | data = self._call(self.host, 'v3/user/link_lookup', params, 442 | self.secret) 443 | return data['data']['link_lookup'] 444 | 445 | def user_link_save(self, longUrl=None, long_url=None, title=None, 446 | note=None, private=None, user_ts=None): 447 | """save a link into the user's history""" 448 | params = dict() 449 | if not longUrl and not long_url: 450 | raise BitlyError('500', 'MISSING_ARG_LONG_URL') 451 | params['longUrl'] = longUrl or long_url 452 | if title is not None: 453 | params['title'] = str(title) 454 | if note is not None: 455 | params['note'] = str(note) 456 | if private is not None: 457 | params['private'] = bool(private) 458 | if user_ts is not None: 459 | params['user_ts'] = user_ts 460 | 461 | data = self._call_oauth2("v3/user/link_save", params) 462 | return data['link_save'] 463 | 464 | def pro_domain(self, domain): 465 | """ is the domain assigned for bitly.pro? """ 466 | end_point = 'v3/bitly_pro_domain' 467 | 468 | if not domain: 469 | raise BitlyError(500, 'MISSING_ARG_DOMAIN') 470 | 471 | protocol_prefix = ('http://', 'https://') 472 | if domain.lower().startswith(protocol_prefix): 473 | raise BitlyError(500, 'INVALID_BARE_DOMAIN') 474 | params = dict(domain=domain) 475 | data = self._call(self.host, end_point, params, self.secret) 476 | return data['data']['bitly_pro_domain'] 477 | 478 | def bundle_archive(self, bundle_link): 479 | """archive a bundle for the authenticated user""" 480 | params = dict(bundle_link=bundle_link) 481 | data = self._call_oauth2_metrics("v3/bundle/archive", params) 482 | return data 483 | 484 | def bundle_bundles_by_user(self, user=None, expand_user=False): 485 | """list bundles by user (defaults to authed user)""" 486 | params = dict() 487 | if user is not None: 488 | params["user"] = user 489 | if expand_user is True: 490 | params["expand_user"] = "true" 491 | data = self._call_oauth2_metrics("v3/bundle/bundles_by_user", params) 492 | return data 493 | 494 | def bundle_clone(self, bundle_link): # TODO: 500s 495 | """clone a bundle for the authenticated user""" 496 | params = dict(bundle_link=bundle_link) 497 | data = self._call_oauth2_metrics("v3/bundle/clone", params) 498 | return data 499 | 500 | def bundle_collaborator_add(self, bundle_link, collaborator=None): 501 | """add a collaborator a bundle""" 502 | params = dict(bundle_link=bundle_link) 503 | if collaborator is not None: 504 | params["collaborator"] = collaborator 505 | data = self._call_oauth2_metrics("v3/bundle/collaborator_add", params) 506 | return data 507 | 508 | def bundle_collaborator_remove(self, bundle_link, collaborator): 509 | """remove a collaborator from a bundle""" 510 | params = dict(bundle_link=bundle_link) 511 | params["collaborator"] = collaborator 512 | data = self._call_oauth2_metrics("v3/bundle/collaborator_remove", 513 | params) 514 | return data 515 | 516 | def bundle_contents(self, bundle_link, expand_user=False): 517 | """list the contents of a bundle""" 518 | params = dict(bundle_link=bundle_link) 519 | if expand_user: 520 | params["expand_user"] = "true" 521 | data = self._call_oauth2_metrics("v3/bundle/contents", params) 522 | return data 523 | 524 | def bundle_create(self, private=False, title=None, description=None): 525 | """create a bundle""" 526 | params = dict() 527 | if private: 528 | params["private"] = "true" 529 | if title is not None: 530 | assert isinstance(title, string_types) 531 | params["title"] = title 532 | if description is not None: 533 | assert isinstance(description, string_types) 534 | params["description"] = description 535 | data = self._call_oauth2_metrics("v3/bundle/create", params) 536 | return data 537 | 538 | def bundle_edit(self, bundle_link, edit=None, title=None, description=None, 539 | private=None, preview=None, og_image=None): 540 | """edit a bundle for the authenticated user""" 541 | params = dict(bundle_link=bundle_link) 542 | if edit: 543 | assert isinstance(edit, string_types) 544 | params["edit"] = edit 545 | if title: 546 | assert isinstance(title, string_types) 547 | params["title"] = title 548 | if description: 549 | assert isinstance(description, string_types) 550 | params["description"] = description 551 | if private is not None: 552 | if private: 553 | params["private"] = "true" 554 | else: 555 | params["private"] = "false" 556 | if preview is not None: 557 | if preview: 558 | params["preview"] = "true" 559 | else: 560 | params["preview"] = "false" 561 | if og_image: 562 | assert isinstance(og_image, string_types) 563 | params["og_image"] = og_image 564 | data = self._call_oauth2_metrics("v3/bundle/edit", params) 565 | return data 566 | 567 | def bundle_link_add(self, bundle_link, link, title=None): 568 | """add a link to a bundle""" 569 | params = dict(bundle_link=bundle_link, link=link) 570 | if title: 571 | assert isinstance(title, string_types) 572 | params["title"] = title 573 | data = self._call_oauth2_metrics("v3/bundle/link_add", params) 574 | return data 575 | 576 | def bundle_link_comment_add(self, bundle_link, link, comment): 577 | """add a comment to a link in a bundle""" 578 | params = dict(bundle_link=bundle_link, link=link, comment=comment) 579 | data = self._call_oauth2_metrics("v3/bundle/link_comment_add", params) 580 | return data 581 | 582 | def bundle_link_comment_edit(self, bundle_link, link, comment_id, comment): 583 | """edit a comment on a link in a bundle""" 584 | params = dict(bundle_link=bundle_link, link=link, 585 | comment_id=comment_id, comment=comment) 586 | data = self._call_oauth2_metrics("v3/bundle/link_comment_edit", params) 587 | return data 588 | 589 | def bundle_link_comment_remove(self, bundle_link, link, comment_id): 590 | """ remove a comment on a link in a bundle""" 591 | params = dict(bundle_link=bundle_link, link=link, 592 | comment_id=comment_id) 593 | data = self._call_oauth2_metrics("v3/bundle/link_comment_remove", 594 | params) 595 | return data 596 | 597 | def bundle_link_edit(self, bundle_link, link, edit, title=None, 598 | preview=None): 599 | """ edit the title for a link """ 600 | params = dict(bundle_link=bundle_link, link=link) 601 | if edit == "title": 602 | params["edit"] = edit 603 | assert isinstance(title, string_types) 604 | params["title"] = title 605 | elif edit == "preview": 606 | params["edit"] = edit 607 | assert isinstance(preview, bool) 608 | if preview: 609 | params["preview"] = "true" 610 | else: 611 | params["preview"] = "false" 612 | else: 613 | raise BitlyError(500, 614 | "PARAM EDIT MUST HAVE VALUE TITLE OR PREVIEW") 615 | data = self._call_oauth2_metrics("v3/bundle/link_edit", params) 616 | return data 617 | 618 | def bundle_link_remove(self, bundle_link, link): 619 | """ remove a link from a bundle """ 620 | params = dict(bundle_link=bundle_link, link=link) 621 | data = self._call_oauth2_metrics("v3/bundle/link_remove", params) 622 | return data 623 | 624 | def bundle_link_reorder(self, bundle_link, link, display_order): 625 | """ reorder the links in a bundle""" 626 | params = dict(bundle_link=bundle_link, link=link, 627 | display_order=display_order) 628 | data = self._call_oauth2_metrics("v3/bundle/link_reorder", params) 629 | return data 630 | 631 | def bundle_pending_collaborator_remove(self, bundle_link, collaborator): 632 | """remove a pending collaborator from a bundle""" 633 | params = dict(bundle_link=bundle_link) 634 | params["collaborator"] = collaborator 635 | data = self._call_oauth2_metrics( 636 | "v3/bundle/pending_collaborator_remove", params) 637 | return data 638 | 639 | def bundle_view_count(self, bundle_link): 640 | """ get the number of views on a bundle """ 641 | params = dict(bundle_link=bundle_link) 642 | data = self._call_oauth2_metrics("v3/bundle/view_count", params) 643 | return data 644 | 645 | def user_bundle_history(self): 646 | """ return the bundles that this user has access to """ 647 | data = self._call_oauth2_metrics("v3/user/bundle_history", dict()) 648 | return data 649 | 650 | def highvalue(self, limit=10, lang='en'): 651 | params = dict(lang=lang) 652 | data = self._call_oauth2_metrics("v3/highvalue", params, limit=limit) 653 | return data 654 | 655 | def realtime_bursting_phrases(self): 656 | data = self._call_oauth2_metrics("v3/realtime/bursting_phrases", 657 | dict()) 658 | return data["phrases"] 659 | 660 | def realtime_hot_phrases(self): 661 | data = self._call_oauth2_metrics("v3/realtime/hot_phrases", dict()) 662 | return data["phrases"] 663 | 664 | def realtime_clickrate(self, phrase): 665 | params = dict(phrase=phrase) 666 | data = self._call_oauth2_metrics("v3/realtime/clickrate", params) 667 | return data["rate"] 668 | 669 | def link_info(self, link): 670 | params = dict(link=link) 671 | data = self._call_oauth2_metrics("v3/link/info", params) 672 | return data 673 | 674 | def link_content(self, link, content_type="html"): 675 | params = dict(link=link, content_type=content_type) 676 | data = self._call_oauth2_metrics("v3/link/content", params) 677 | return data["content"] 678 | 679 | def link_category(self, link): 680 | params = dict(link=link) 681 | data = self._call_oauth2_metrics("v3/link/category", params) 682 | return data["categories"] 683 | 684 | def link_social(self, link): 685 | params = dict(link=link) 686 | data = self._call_oauth2_metrics("v3/link/social", params) 687 | return data["social_scores"] 688 | 689 | def link_location(self, link): 690 | params = dict(link=link) 691 | data = self._call_oauth2_metrics("v3/link/location", params) 692 | return data["locations"] 693 | 694 | def link_language(self, link): 695 | params = dict(link=link) 696 | data = self._call_oauth2_metrics("v3/link/language", params) 697 | return data["languages"] 698 | 699 | def search(self, query, offset=None, cities=None, domain=None, fields=None, 700 | limit=10, lang='en'): 701 | params = dict(query=query, lang=lang) 702 | if offset: 703 | assert isinstance(offset, integer_types) 704 | params["offset"] = str(offset) 705 | if cities: # TODO: check format 706 | assert isinstance(cities, string_types) 707 | params["cities"] = cities 708 | if domain: 709 | assert isinstance(domain, string_types) 710 | params["domain"] = domain 711 | if fields: 712 | assert isinstance(fields, string_types) 713 | params["fields"] = fields 714 | data = self._call_oauth2_metrics("v3/search", params, limit=limit) 715 | return data['results'] 716 | 717 | @classmethod 718 | def _generateSignature(self, params, secret): 719 | if not params or not secret: 720 | return "" 721 | hash_string = "" 722 | if not params.get('t'): 723 | # note, this uses a utc timestamp not a local timestamp 724 | params['t'] = str(int(time.mktime(time.gmtime()))) 725 | 726 | keys = params.keys() 727 | keys.sort() 728 | for k in keys: 729 | if type(params[k]) in [types.ListType, types.TupleType]: 730 | for v in params[k]: 731 | hash_string += v 732 | else: 733 | hash_string += params[k] 734 | hash_string += secret 735 | signature = hashlib.md5(hash_string).hexdigest()[:10] 736 | return signature 737 | 738 | def _call_oauth2_metrics(self, endpoint, params, unit=None, units=None, 739 | tz_offset=None, rollup=None, limit=None, 740 | unit_reference_ts=None): 741 | if unit is not None: 742 | assert unit in ("minute", "hour", "day", "week", "mweek", "month") 743 | params["unit"] = unit 744 | if units is not None: 745 | assert isinstance(units, integer_types), \ 746 | "Unit (%r) must be integer" % units 747 | params["units"] = units 748 | if tz_offset is not None: 749 | # tz_offset can either be a hour offset, or a timezone like 750 | # North_America/New_York 751 | if isinstance(tz_offset, integer_types): 752 | msg = "integer tz_offset must be between -12 and 12" 753 | assert -12 <= tz_offset <= 12, msg 754 | else: 755 | assert isinstance(tz_offset, string_types) 756 | params["tz_offset"] = tz_offset 757 | if rollup is not None: 758 | assert isinstance(rollup, bool) 759 | params["rollup"] = "true" if rollup else "false" 760 | if limit is not None: 761 | assert isinstance(limit, integer_types) 762 | params["limit"] = limit 763 | if unit_reference_ts is not None: 764 | assert (unit_reference_ts == 'now' or 765 | isinstance(unit_reference_ts, integer_types)) 766 | params["unit_reference_ts"] = unit_reference_ts 767 | 768 | return self._call_oauth2(endpoint, params) 769 | 770 | def _call_oauth2(self, endpoint, params): 771 | assert self.access_token, "This %s endpoint requires OAuth" % endpoint 772 | return self._call(self.ssl_host, endpoint, params)["data"] 773 | 774 | def _call(self, host, method, params, secret=None, timeout=5000): 775 | params['format'] = params.get('format', 'json') # default to json 776 | 777 | if self.access_token: 778 | scheme = 'https' 779 | params['access_token'] = self.access_token 780 | host = self.ssl_host 781 | else: 782 | scheme = 'http' 783 | params['login'] = self.login 784 | params['apiKey'] = self.api_key 785 | 786 | if secret: 787 | params['signature'] = self._generateSignature(params, secret) 788 | 789 | # force to utf8 to fix ascii codec errors 790 | params = _utf8_params(params) 791 | 792 | request = "%(scheme)s://%(host)s/%(method)s?%(params)s" % { 793 | 'scheme': scheme, 794 | 'host': host, 795 | 'method': method, 796 | 'params': urlencode(params, doseq=1) 797 | } 798 | 799 | try: 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) 810 | if data.get('status_code', 500) != 200: 811 | raise BitlyError(data.get('status_code', 500), 812 | data.get('status_txt', 'UNKNOWN_ERROR')) 813 | 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()) 818 | except BitlyError: 819 | raise 820 | except Exception: 821 | raise BitlyError(None, sys.exc_info()[1]) 822 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then 4 | export BITLY_ACCESS_TOKEN="$SECURE_ACCESS_TOKEN" 5 | fi 6 | 7 | if [ -n "$BITLY_ACCESS_TOKEN" ]; then 8 | nosetests 9 | fi 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | version = '0.3' 4 | 5 | setup(name='bitly_api', 6 | version=version, 7 | description="An API for bit.ly", 8 | long_description=open("./README.md", "r").read(), 9 | classifiers=[ 10 | "Development Status :: 5 - Production/Stable", 11 | "Environment :: Console", 12 | "Intended Audience :: End Users/Desktop", 13 | "Natural Language :: English", 14 | "Operating System :: OS Independent", 15 | "Programming Language :: Python", 16 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", 17 | "Topic :: Utilities", 18 | "License :: OSI Approved :: Apache Software License", 19 | ], 20 | keywords='bitly bit.ly', 21 | author='Jehiah Czebotar', 22 | author_email='jehiah@gmail.com', 23 | url='https://github.com/bitly/bitly-api-python', 24 | license='Apache Software License', 25 | packages=['bitly_api'], 26 | include_package_data=True, 27 | zip_safe=True, 28 | ) 29 | -------------------------------------------------------------------------------- /test/test_bitly_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | """ 3 | This is a py.test script 4 | 5 | Example usage on Unix: 6 | bitly-api-python $ BITLY_ACCESS_TOKEN= nosetests 7 | or 'export' the two environment variables prior to running nosetests 8 | """ 9 | import os 10 | import sys 11 | sys.path.append('../') 12 | import bitly_api 13 | 14 | BITLY_ACCESS_TOKEN = "BITLY_ACCESS_TOKEN" 15 | 16 | 17 | def get_connection(): 18 | """Create a Connection base on username and access token credentials""" 19 | if BITLY_ACCESS_TOKEN not in os.environ: 20 | raise ValueError("Environment variable '{}' required".format(BITLY_ACCESS_TOKEN)) 21 | access_token = os.getenv(BITLY_ACCESS_TOKEN) 22 | bitly = bitly_api.Connection(access_token=access_token) 23 | return bitly 24 | 25 | 26 | def testApi(): 27 | bitly = get_connection() 28 | data = bitly.shorten('http://google.com/') 29 | assert data is not None 30 | assert data['long_url'] == 'http://google.com/' 31 | assert data['hash'] is not None 32 | 33 | 34 | def testExpand(): 35 | bitly = get_connection() 36 | data = bitly.expand(hash='test1_random_fjslfjieljfklsjflkas') 37 | assert data is not None 38 | assert len(data) == 1 39 | assert data[0]['error'] == 'NOT_FOUND' 40 | 41 | 42 | def testReferrer(): 43 | bitly = get_connection() 44 | data = bitly.referrers(hash='a') 45 | assert data is not None 46 | assert len(data) > 1 47 | 48 | 49 | def testProDomain(): 50 | bitly = get_connection() 51 | test_data = { 52 | 'cnn.com': False, 53 | 'nyti.ms': True, 54 | 'g.co': False, 55 | 'j.mp': False, 56 | 'pep.si': True, 57 | 'http://pep.si': 'INVALID_BARE_DOMAIN', 58 | } 59 | for domain in test_data: 60 | try: 61 | result = bitly.pro_domain(domain) 62 | assert result == test_data[domain], domain 63 | except bitly_api.BitlyError as e: 64 | 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 72 | --------------------------------------------------------------------------------