Skip to content

Commit

Permalink
Merge daf4f4a into ee33b2f
Browse files Browse the repository at this point in the history
  • Loading branch information
calmrat committed Oct 6, 2015
2 parents ee33b2f + daf4f4a commit d526dab
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -8,7 +8,7 @@ python:
before_install:
- "pip install -U pip setuptools virtualenv coveralls"
install:
- "python setup.py install"
- "pip install -e .[bitly]"
- "git fetch --unshallow"
script:
- "coverage run --source=bin,did -m py.test $CAPTURE tests"
Expand Down
3 changes: 3 additions & 0 deletions did/plugins/__init__.py
Expand Up @@ -28,6 +28,8 @@
+----------+-----+
| wiki | 700 |
+----------+-----+
| bitly | 701 |
+----------+-----+
| items | 800 |
+----------+-----+
| footer | 900 |
Expand All @@ -51,6 +53,7 @@

FAILED_PLUGINS = []


def load():
""" Check available plugins and attempt to import them """
# Code is based on beaker-client's command.py script
Expand Down
148 changes: 148 additions & 0 deletions did/plugins/bitly.py
@@ -0,0 +1,148 @@
#!/usr/bin/env python
# coding: utf-8
# Author: "Chris Ward" <cward@redhat.com>

"""
Bit.ly stats such as:
* links saved
Config example::
[bitly]
type = bitly
token = ...
To get a token, see: https://bitly.com/a/oauth_apps
Available options:
--bitly-saved Links Saved
--bitly All above
"""

from __future__ import absolute_import, unicode_literals

import time

from bitly_api import Connection

from did.stats import Stats, StatsGroup
from did.utils import log, pretty
from did.base import Config, ReportError, ConfigError

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# bit.ly
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

######
# NOTES
# bitly_api Usage
# import bitly_api
# c = bitly_api.Connection(access_token='...')
# hist = c.user_link_history(created_)

# user_link_history arguments
# created_before=None, created_after=None,
# archived=None, limit=None, offset=None, private=None
######


class Bitly(object):
""" Bit.ly Link History """

_connection = None

def __init__(self, parent, token=None):
""" Initialize bit.ly OAuth Connection """
self.parent = parent
self.token = token or getattr(parent, 'token')
if not self.token:
msg = "bitly requires token to be defined in config"
log.error(msg)
raise ConfigError(msg)

@property
def api(self):
if not self._connection:
self._connection = Connection(access_token=self.token)
return self._connection

def user_link_history(self, created_before=None, created_after=None,
limit=100, **kwargs):
""" Bit.ly API - user_link_history wrapper"""
""" Bit.ly link
Link History Keys
-----------------
[u'aggregate_link', u'archived', u'campaign_ids',
u'client_id', u'created_at', u'keyword_link',
u'link', u'long_url', u'modified_at',
u'private', u'tags', u'title', u'user_ts']
"""
# bit.ly API doesn't seem to like anything other than int's
limit = int(limit)
created_after = int(created_after)
created_before = int(created_before)
hist = self.api.user_link_history(
limit=limit, created_before=created_before,
created_after=created_after)

# FIXME: if we have more than 100 objects we need to PAGINATE
record = "{0} - {1}"
links = []
for r in hist:
link = r.get('keyword_link') or r['link']
title = r['title'] or '<< NO TITLE >>'
links.append(record.format(link, title))
log.debug("First 3 Links fetched:")
log.debug(pretty(hist[0:3], indent=4))
return links


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Bit.ly Link Stats
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

class SavedLinks(Stats):
""" Links saved """
def fetch(self):
'''
Bit.ly API expect unix timestamps
'''
since = time.mktime(self.options.since.datetime.timetuple())
until = time.mktime(self.options.until.datetime.timetuple())
log.info("Searching for links saved by {0}".format(self.user))
self.stats = self.parent.bitly.user_link_history(created_after=since,
created_before=until)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Bit.ly Link Stats Group
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

class BitlyStats(StatsGroup):
""" Bit.ly """

# Default order
order = 701

def __init__(self, option, name=None, parent=None, user=None):
""" Process config, prepare investigator, construct stats """

# Check Request Tracker instance url and custom prefix
super(BitlyStats, self).__init__(option, name, parent, user)
config = dict(Config().section(option))

try:
self.token = config["token"]
except KeyError:
raise ReportError(
"No token in the [{0}] section".format(option))

self.bitly = Bitly(parent=self)
# Construct the list of stats
self.stats = [
SavedLinks(option=option + "-saved", parent=self),
]
1 change: 1 addition & 0 deletions did/utils.py
Expand Up @@ -4,6 +4,7 @@

from __future__ import unicode_literals, absolute_import

from dateutil.parser import parse as dt_parse
import os
import re
import sys
Expand Down
7 changes: 7 additions & 0 deletions docs/plugins.rst
Expand Up @@ -97,3 +97,10 @@ wiki
.. automodule:: did.plugins.wiki
:members:
:undoc-members:

bitly
-----

.. automodule:: did.plugins.bitly
:members:
:undoc-members:
5 changes: 5 additions & 0 deletions examples/config
Expand Up @@ -56,6 +56,11 @@ url = https://issues.jboss.org/
type = wiki
wiki test = http://moinmo.in/

# MUST SET token FOR BITLY PLUGIN TO WORK
[bitly]
type = bitly
token = SEE: https://bitly.com/a/oauth_apps

[projects]
type = items
header = Work on projects
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Expand Up @@ -42,6 +42,9 @@
'bootstrap': [
'sphinx_bootstrap_theme',
],
'bitly': [
'bitly_api',
],
}

pip_src = 'https://pypi.python.org/packages/source'
Expand Down
78 changes: 78 additions & 0 deletions tests/plugins/test_bitly.py
@@ -0,0 +1,78 @@
# coding: utf-8
""" Tests for the Bitly plugin """

from __future__ import unicode_literals, absolute_import

import pytest

from bitly_api import BitlyError

from did.base import ReportError

BASIC_CONFIG = """
[general]
email = "Chris Ward" <cward@redhat.com>
[bitly]
type = bitly
"""

BAD_TOKEN_CONFIG = BASIC_CONFIG + "\ntoken = bad-token"
# test token created by "Chris Ward" <kejbaly2+did@gmail.com>
OK_CONFIG = BASIC_CONFIG + "\ntoken = 77912602cc1d712731b2d8a2810cf8500d2d0f89"

# one link should be present
INTERVAL = "--since 2015-10-06 --until 2015-10-07"
# No links should be present
INTERVAL1 = "--since 2015-10-07 --until 2015-10-08"


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Smoke tests
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

def test_import():
""" Test basic module import """
from did.plugins.bitly import BitlyStats
assert BitlyStats


def test_missing_token():
"""
Missing bitly token results in Exception
"""
import did
did.base.Config(BASIC_CONFIG)
# FIXME: is SystemExit really a reasonable exception?
# why not let the exception that occured just happen?
# why the use of sys.exit?
# Testing required that we check for SystemExit exception
# even though that's not the actual error that is triggered
with pytest.raises(ReportError):
did.cli.main(INTERVAL)


def test_invalid_token():
""" Invalid bitly token """
import did
did.base.Config(BAD_TOKEN_CONFIG)
with pytest.raises(BitlyError):
did.cli.main(INTERVAL)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Acceptance tests
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

def test_bitly_saved():
""" Check expected saved links are returned """
import did
did.base.Config(OK_CONFIG)
result = did.cli.main(INTERVAL)
stats = result[0][0].stats[0].stats[0].stats
_m = (
"http://bit.ly/kejbaly2_roreilly_innerwars_reddit - "
"Quest to decode and play my brother's INNER WARS album : bucketlist"
)
assert len(stats) == 1
assert stats[0] == _m

0 comments on commit d526dab

Please sign in to comment.