Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add handler for rendering pages #7

Merged
merged 6 commits into from

2 participants

@miltontony
Owner

e.g Top 10 User Agent strings

@jerith
Owner

:+1:

shortener/models.py
@@ -1,7 +1,7 @@
import hashlib
from datetime import datetime
-from sqlalchemy import Column, Integer, String, Text, DateTime
+from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
@jerith Owner
jerith added a note

Unused import.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jerith
Owner

:+1:

@miltontony miltontony merged commit a9ead7d into from
@miltontony miltontony deleted the branch
@miltontony miltontony restored the branch
@miltontony miltontony deleted the branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
24 shortener/api.py
@@ -21,6 +21,19 @@ def __init__(self, reactor, config):
self.config = config
self.engine = get_engine(config['connection_string'], reactor)
self.metrics = ShortenerMetrics(reactor, config)
+ self.load_handlers()
+
+ def load_handlers(self):
+ self.handlers = {}
+ for handler_config in self.config['handlers']:
+ [(name, class_path)] = handler_config.items()
+ parts = class_path.split('.')
+ module = '.'.join(parts[:-1])
+ class_name = parts[-1]
+ handler_module = __import__(module, fromlist=[class_name])
+ handler_class = getattr(handler_module, class_name)
+ handler = handler_class(self.config, self.engine)
+ self.handlers[name] = handler
@handler('/api/create', methods=['PUT'])
@inlineCallbacks
@@ -52,6 +65,17 @@ def init_account(self, request):
returnValue({'created': not already_exists})
+ @handler('/api/handler/<string:handler_name>', methods=['GET'])
+ @inlineCallbacks
+ def run_handler(self, request, handler_name):
+ handler = self.handlers.get(handler_name)
+ if not handler:
+ request.setResponseCode(http.NOT_FOUND)
+ returnValue({})
+ else:
+ response = yield handler.render(request)
+ returnValue(response)
+
@handler('/<string:short_url>', methods=['GET'])
@inlineCallbacks
def resolve_url(self, request, short_url):
View
0  shortener/handlers/__init__.py
No changes.
View
12 shortener/handlers/base.py
@@ -0,0 +1,12 @@
+class BaseApiHandler(object):
+ def __init__(self, config, db_engine):
+ self.config = config
+ self.engine = db_engine
+
+ def render(self):
+ """
+ Generates a dict that will be returned as json
+
+ Should return a Dict
+ """
+ raise NotImplementedError('Subclasses should implement this.')
View
37 shortener/handlers/dump.py
@@ -0,0 +1,37 @@
+from twisted.internet.defer import inlineCallbacks, returnValue
+from shortener.handlers.base import BaseApiHandler
+from shortener.models import ShortenerTables
+from twisted.web import http
+
+
+class Dump(BaseApiHandler):
+ def _format(self, row, audit):
+ return {
+ 'domain': row['domain'],
+ 'user_token': row['user_token'],
+ 'short_url': row['short_url'],
+ 'long_url': row['long_url'],
+ 'created_at': row['created_at'].isoformat(),
+ 'hits': audit['hits']
+ }
+
+ @inlineCallbacks
+ def render(self, request):
+ short_url = request.args.get('url')
+ conn = yield self.engine.connect()
+ try:
+ tables = ShortenerTables(self.config['account'], conn)
+ if not short_url:
+ request.setResponseCode(http.BAD_REQUEST)
+ returnValue({'error': 'expected "?url=<short_url>"'})
+ else:
+ row = yield tables.get_row_by_short_url(short_url[0], False)
+
+ if row:
+ audit = yield tables.get_audit_row(row['id'])
+ returnValue(self._format(row, audit))
+ else:
+ request.setResponseCode(http.NOT_FOUND)
+ returnValue({'error': 'short url not found'})
+ finally:
+ yield conn.close()
View
64 shortener/models.py
@@ -17,7 +17,7 @@ class NoShortenerTables(ShortenerDBError):
class ShortenerTables(TableCollection):
- shortened_urls = make_table(
+ urls = make_table(
Column("id", Integer(), primary_key=True),
Column("domain", String(255), nullable=False, index=True),
Column("user_token", String(255), nullable=False, index=True),
@@ -27,6 +27,12 @@ class ShortenerTables(TableCollection):
Column("created_at", DateTime(timezone=False))
)
+ audit = make_table(
+ Column("id", Integer(), primary_key=True),
+ Column('url_id', Integer(), nullable=False),
+ Column("hits", Integer()),
+ )
+
def _format_row(self, row, fields=None):
if row is None:
return None
@@ -50,16 +56,16 @@ def get_or_create_row(self, domain, user_token, long_url):
domain, user_token, long_url
])).hexdigest()
result = yield self.execute_query(
- self.shortened_urls.select().where(and_(
- self.shortened_urls.c.domain == domain,
- self.shortened_urls.c.user_token == user_token,
- self.shortened_urls.c.hash == hashkey
+ self.urls.select().where(and_(
+ self.urls.c.domain == domain,
+ self.urls.c.user_token == user_token,
+ self.urls.c.hash == hashkey
)).limit(1))
row = yield result.fetchone()
if not row:
yield self.execute_query(
- self.shortened_urls.insert().values(
+ self.urls.insert().values(
domain=domain,
user_token=user_token,
hash=hashkey,
@@ -69,25 +75,57 @@ def get_or_create_row(self, domain, user_token, long_url):
# We need to return the inserted row
result = yield self.execute_query(
- self.shortened_urls.select().where(
- self.shortened_urls.c.hash == hashkey
+ self.urls.select().where(
+ self.urls.c.hash == hashkey
).limit(1))
row = yield result.fetchone()
+ yield self.create_audit(row['id'])
+
returnValue(self._format_row(row))
@inlineCallbacks
def update_short_url(self, row_id, short_url):
yield self.execute_query(
- self.shortened_urls.update().where(
- self.shortened_urls.c.id == row_id
+ self.urls.update().where(
+ self.urls.c.id == row_id
).values(short_url=short_url))
@inlineCallbacks
- def get_row_by_short_url(self, short_url):
+ def get_row_by_short_url(self, short_url, increment=True):
+ result = yield self.execute_query(
+ self.urls.select().where(
+ self.urls.c.short_url == short_url
+ ).limit(1))
+ row = yield result.fetchone()
+
+ if row and increment:
+ yield self.execute_query(
+ self.audit.update().where(
+ self.audit.c.url_id == row['id']
+ ).values(hits=self.audit.c.hits + 1)
+ )
+ returnValue(self._format_row(row))
+
+ @inlineCallbacks
+ def create_audit(self, url_id):
+ result = yield self.execute_query(
+ self.audit.select().where(
+ self.audit.c.url_id == url_id
+ ).limit(1))
+ row = yield result.fetchone()
+
+ if not row:
+ yield self.execute_query(
+ self.audit.insert().values(url_id=url_id, hits=0)
+ )
+ returnValue({})
+
+ @inlineCallbacks
+ def get_audit_row(self, url_id):
result = yield self.execute_query(
- self.shortened_urls.select().where(
- self.shortened_urls.c.short_url == short_url
+ self.audit.select().where(
+ self.audit.c.url_id == url_id
).limit(1))
row = yield result.fetchone()
returnValue(self._format_row(row))
View
18 shortener/tests/test_api.py
@@ -38,6 +38,9 @@ def setUp(self):
'account': self.account,
'connection_string': connection_string,
'graphite_endpoint': 'tcp:www.example.com:80',
+ 'handlers': [
+ {'dump': 'shortener.handlers.dump.Dump'},
+ ],
}
self.pool = HTTPConnectionPool(reactor, persistent=False)
self.service = ShortenerServiceApp(
@@ -203,6 +206,21 @@ def test_resolve_url(self):
self.assertEqual(result['long_url'], url + '4')
@inlineCallbacks
+ def test_resolve_url_hits_counter(self):
+ tables = ShortenerTables(self.account, self.conn)
+ yield tables.create_tables()
+
+ url = 'http://en.wikipedia.org/wiki/Cthulhu'
+ yield self.service.shorten_url(url)
+
+ yield self.service.get_row_by_short_url('qr0')
+ yield self.service.get_row_by_short_url('qr0')
+ result = yield self.service.get_row_by_short_url('qr0')
+
+ audit = yield tables.get_audit_row(result['id'])
+ self.assertEqual(audit['hits'], 3)
+
+ @inlineCallbacks
def test_short_url_sequencing(self):
yield ShortenerTables(self.account, self.conn).create_tables()
View
108 shortener/tests/test_handlers.py
@@ -0,0 +1,108 @@
+import json
+import os
+import treq
+
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+from twisted.web.client import HTTPConnectionPool
+from twisted.trial.unittest import TestCase
+from twisted.web.server import Site
+
+from aludel.database import MetaData
+from shortener.api import ShortenerServiceApp
+from shortener.models import ShortenerTables
+from shortener.metrics import CarbonClientService
+from shortener.tests.doubles import (
+ DisconnectingStringTransport, StringTransportClientEndpoint)
+
+
+class TestHandlers(TestCase):
+ timeout = 5
+
+ def _drop_tables(self):
+ # NOTE: This is a blocking operation!
+ md = MetaData(bind=self.service.engine._engine)
+ md.reflect()
+ md.drop_all()
+ assert self.service.engine._engine.table_names() == []
+
+ @inlineCallbacks
+ def setUp(self):
+ reactor.suggestThreadPoolSize(1)
+ connection_string = os.environ.get(
+ "SHORTENER_TEST_CONNECTION_STRING", "sqlite://")
+
+ self.account = 'test-account'
+ cfg = {
+ 'host_domain': 'http://wtxt.io',
+ 'account': self.account,
+ 'connection_string': connection_string,
+ 'graphite_endpoint': 'tcp:www.example.com:80',
+ 'handlers': [
+ {'dump': 'shortener.handlers.dump.Dump'},
+ ],
+ }
+ self.pool = HTTPConnectionPool(reactor, persistent=False)
+ self.service = ShortenerServiceApp(
+ reactor=reactor,
+ config=cfg
+ )
+
+ self.tr = DisconnectingStringTransport()
+ endpoint = StringTransportClientEndpoint(reactor, self.tr)
+ self.service.metrics.carbon_client = CarbonClientService(endpoint)
+ self.service.metrics.carbon_client.startService()
+ yield self.service.metrics.carbon_client.connect_d
+
+ site = Site(self.service.app.resource())
+ self.listener = reactor.listenTCP(0, site, interface='localhost')
+ self.listener_port = self.listener.getHost().port
+ self._drop_tables()
+ self.conn = yield self.service.engine.connect()
+ self.addCleanup(self.listener.loseConnection)
+ self.addCleanup(self.pool.closeCachedConnections)
+
+ def make_url(self, path):
+ return 'http://localhost:%s%s' % (self.listener_port, path)
+
+ @inlineCallbacks
+ def test_api_dump(self):
+ yield ShortenerTables(self.account, self.conn).create_tables()
+
+ url = 'http://en.wikipedia.org/wiki/Cthulhu'
+ yield self.service.shorten_url(url, 'test-user')
+ yield treq.get(
+ self.make_url('/qr0'),
+ allow_redirects=False,
+ pool=self.pool)
+
+ resp = yield treq.get(
+ self.make_url('/api/handler/dump?url=qr0'),
+ allow_redirects=False,
+ pool=self.pool)
+
+ self.assertEqual(resp.code, 200)
+ result = yield treq.json_content(resp)
+ self.assertEqual(result['user_token'], 'test-user')
+ self.assertEqual(result['short_url'], 'qr0')
+ self.assertEqual(result['long_url'], url)
+ self.assertEqual(result['hits'], 1)
+ self.assertEqual(result['domain'], 'en.wikipedia.org')
+
+ @inlineCallbacks
+ def test_api_dump_invalid_querystring(self):
+ yield ShortenerTables(self.account, self.conn).create_tables()
+
+ url = 'http://en.wikipedia.org/wiki/Cthulhu'
+ yield self.service.shorten_url(url, 'test-user')
+ yield treq.get(
+ self.make_url('/qr0'),
+ allow_redirects=False,
+ pool=self.pool)
+
+ resp = yield treq.get(
+ self.make_url('/api/handler/dump'),
+ allow_redirects=False,
+ pool=self.pool)
+
+ self.assertEqual(resp.code, 400)
Something went wrong with that request. Please try again.