├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── flask_shorturl.py ├── setup.py └── test_shorturl.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.egg-info 4 | __pycache__ 5 | bin 6 | build 7 | develop-eggs 8 | dist 9 | eggs 10 | parts 11 | .DS_Store 12 | .installed.cfg 13 | docs/_build 14 | cover/ 15 | .tox 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | install: 3 | - pip install flask 4 | 5 | python: 6 | - "2.7" 7 | - "pypy" 8 | - "3.4" 9 | - "3.5" 10 | 11 | script: 12 | - nosetests -s 13 | 14 | after_success: 15 | - pip install coveralls 16 | - coverage run --source=flask_shorturl setup.py -q nosetests 17 | - coveralls 18 | 19 | notifications: 20 | email: false 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Hsiaoming Yang 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | * Neither the name of the creator nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs 2 | 3 | all: 4 | @pip install -r dev-reqs.txt 5 | 6 | lint: 7 | @flake8 8 | 9 | test: 10 | @nosetests -s 11 | 12 | coverage: 13 | @rm -f .coverage 14 | @nosetests --with-coverage --cover-package=flask_shorturl --cover-html 15 | 16 | clean: clean-build clean-pyc clean-docs 17 | 18 | 19 | clean-build: 20 | @rm -fr build/ 21 | @rm -fr dist/ 22 | @rm -fr *.egg-info 23 | 24 | 25 | clean-pyc: 26 | @find . -name '*.pyc' -exec rm -f {} + 27 | @find . -name '*.pyo' -exec rm -f {} + 28 | @find . -name '*~' -exec rm -f {} + 29 | @find . -name '__pycache__' -exec rm -fr {} + 30 | 31 | clean-docs: 32 | @rm -fr docs/_build 33 | 34 | docs: 35 | @$(MAKE) -C docs html 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-ShortUrl 2 | ================================== 3 | 4 | .. image:: https://img.shields.io/badge/donate-lepture-green.svg 5 | :target: https://lepture.herokuapp.com/?amount=500&reason=lepture%2Fflask-shorturl 6 | :alt: Donate lepture 7 | .. image:: https://img.shields.io/pypi/wheel/flask-shorturl.svg 8 | :target: https://pypi.python.org/pypi/flask-shorturl/ 9 | :alt: Wheel Status 10 | .. image:: https://img.shields.io/pypi/v/flask-shorturl.svg 11 | :target: https://pypi.python.org/pypi/flask-shorturl/ 12 | :alt: Latest Version 13 | .. image:: https://travis-ci.org/lepture/flask-shorturl.svg?branch=master 14 | :target: https://travis-ci.org/lepture/flask-shorturl 15 | :alt: Travis CI Status 16 | .. image:: https://coveralls.io/repos/lepture/flask-shorturl/badge.svg?branch=master 17 | :target: https://coveralls.io/r/lepture/flask-shorturl 18 | :alt: Coverage Status 19 | 20 | 21 | Short URL generator for Flask Project. 22 | 23 | 24 | Installation 25 | ------------ 26 | 27 | To install Flask-Shorturl, simply:: 28 | 29 | $ pip install Flask-ShortUrl 30 | 31 | Or alternatively if you don't have pip:: 32 | 33 | $ easy_install Flask-ShortUrl 34 | 35 | 36 | Usage 37 | ----- 38 | 39 | You can initialize the app:: 40 | 41 | from flask_shorturl import ShortUrl 42 | 43 | su = ShortUrl(app) 44 | 45 | url = su.encode_url(12) 46 | uid = su.decode_url(url) 47 | 48 | You may also init the app later:: 49 | 50 | su = ShortUrl() 51 | su.init_app(app) 52 | 53 | 54 | Configuration 55 | -------------- 56 | 57 | Configurations for Flask project: 58 | 59 | 60 | ====================== ===================================================== 61 | `SHORT_URL_ALPHABET` The alphabet to be used by Encoder, 62 | default value: ``mn6j2c4rv8bpygw95z7hsdaetxuk3fq`` 63 | `SHORT_URL_MIN_LENGTH` default value: 5 64 | `SHORT_URL_BLOCK_SIZE` default value: 24 65 | ====================== ===================================================== 66 | 67 | 68 | Thanks 69 | ------ 70 | 71 | UrlEncoder from by `Michael Fogleman`_. 72 | 73 | .. _`Michael Fogleman`: http://code.activestate.com/recipes/576918/ 74 | -------------------------------------------------------------------------------- /flask_shorturl.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from werkzeug.routing import BaseConverter, ValidationError 4 | 5 | DEFAULT_ALPHABET = 'mn6j2c4rv8bpygw95z7hsdaetxuk3fq' 6 | MIN_LENGTH = 5 7 | 8 | 9 | __all__ = ['ShortUrl'] 10 | 11 | 12 | class UrlEncoder(object): 13 | """ 14 | Author: Michael Fogleman 15 | License: MIT 16 | Link: http://code.activestate.com/recipes/576918/ (r3) 17 | """ 18 | 19 | def __init__(self, alphabet, block_size=24): 20 | self.alphabet = alphabet 21 | self.block_size = block_size 22 | self.mask = (1 << block_size) - 1 23 | self.mapping = list(range(block_size)) 24 | self.mapping.reverse() 25 | 26 | def encode_url(self, n, min_length=MIN_LENGTH): 27 | return self.enbase(self.encode(n), min_length) 28 | 29 | def decode_url(self, n): 30 | return self.decode(self.debase(n)) 31 | 32 | def encode(self, n): 33 | return (n & ~self.mask) | self._encode(n & self.mask) 34 | 35 | def _encode(self, n): 36 | result = 0 37 | for i, b in enumerate(self.mapping): 38 | if n & (1 << i): 39 | result |= (1 << b) 40 | return result 41 | 42 | def decode(self, n): 43 | return (n & ~self.mask) | self._decode(n & self.mask) 44 | 45 | def _decode(self, n): 46 | result = 0 47 | for i, b in enumerate(self.mapping): 48 | if n & (1 << b): 49 | result |= (1 << i) 50 | return result 51 | 52 | def enbase(self, x, min_length=MIN_LENGTH): 53 | result = self._enbase(x) 54 | padding = self.alphabet[0] * (min_length - len(result)) 55 | return '%s%s' % (padding, result) 56 | 57 | def _enbase(self, x): 58 | n = len(self.alphabet) 59 | if x < n: 60 | return self.alphabet[int(x)] 61 | return self._enbase(int(x / n)) + self.alphabet[int(x % n)] 62 | 63 | def debase(self, x): 64 | n = len(self.alphabet) 65 | result = 0 66 | for i, c in enumerate(reversed(x)): 67 | result += self.alphabet.index(c) * (n ** i) 68 | return result 69 | 70 | 71 | def ShortUrlConverter_factory(su): # noqa 72 | class ShortUrlConverter(BaseConverter): 73 | """ 74 | ShortUrl converter for the Werkzeug routing system. 75 | """ 76 | 77 | def __init__(self, map): 78 | super(ShortUrlConverter, self).__init__(map) 79 | self._su = su 80 | 81 | def to_python(self, value): 82 | try: 83 | return self._su.decode_url(value) 84 | except ValueError: 85 | raise ValidationError() 86 | 87 | def to_url(self, value): 88 | return self._su.encode_url(value) 89 | 90 | return ShortUrlConverter 91 | 92 | 93 | class ShortUrl(object): 94 | """ 95 | ShortUrl Interface. 96 | 97 | Create an instance is simple:: 98 | 99 | su = ShortUrl(app) 100 | 101 | You can also pass the app of Flask later:: 102 | 103 | su = ShortUrl() 104 | su.init_app(app) 105 | 106 | :param app: the app instance of Flask 107 | """ 108 | 109 | def __init__(self, app=None): 110 | if app is not None: 111 | self.init_app(app) 112 | else: 113 | self.app = None 114 | 115 | def init_app(self, app): 116 | app.config.setdefault('SHORT_URL_ALPHABET', DEFAULT_ALPHABET) 117 | app.config.setdefault('SHORT_URL_MIN_LENGTH', MIN_LENGTH) 118 | app.config.setdefault('SHORT_URL_BLOCK_SIZE', 24) 119 | 120 | self.app = app 121 | app.extensions = getattr(app, 'extensions', {}) 122 | app.extensions['short_url'] = self 123 | app.url_map.converters['short_url'] = ShortUrlConverter_factory(self) 124 | 125 | def get_app(self): 126 | if self.app is not None: 127 | return self.app 128 | 129 | from flask import _app_ctx_stack 130 | ctx = _app_ctx_stack.top 131 | if ctx is not None: 132 | return ctx.app 133 | raise RuntimeError( 134 | 'application not registered on ShortUrl ' 135 | 'instance and no application bound to current context' 136 | ) 137 | 138 | @property 139 | def encoder(self): 140 | if hasattr(self, '_encoder'): 141 | return self._encoder 142 | 143 | app = self.get_app() 144 | alphabet = app.config.get('SHORT_URL_ALPHABET') 145 | block_size = app.config.get('SHORT_URL_BLOCK_SIZE', 24) 146 | self._encoder = UrlEncoder(alphabet=alphabet, block_size=block_size) 147 | return self._encoder 148 | 149 | def encode(self, n): 150 | return self.encoder.encode(n) 151 | 152 | def decode(self, n): 153 | return self.encoder.decode(n) 154 | 155 | def enbase(self, n): 156 | app = self.get_app() 157 | min_length = app.config.get('SHORT_URL_MIN_LENGTH') 158 | return self.encoder.enbase(n, min_length) 159 | 160 | def debase(self, n): 161 | return self.encoder.debase(n) 162 | 163 | def encode_url(self, n): 164 | """ 165 | Encode the id number to a short url. 166 | 167 | :: 168 | 169 | >>> su = ShortUrl() 170 | >>> su.encode_url(12) 171 | """ 172 | app = self.get_app() 173 | min_length = app.config.get('SHORT_URL_MIN_LENGTH') 174 | return self.encoder.encode_url(n, min_length) 175 | 176 | def decode_url(self, n): 177 | """ 178 | Decode the short url into the id number. 179 | 180 | :: 181 | 182 | >>> su = ShortUrl() 183 | >>> su.decode_url('zkf2n') 184 | """ 185 | return self.encoder.decode_url(n) 186 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from setuptools import setup 5 | 6 | setup( 7 | name='Flask-ShortUrl', 8 | version='0.2.0', 9 | url='https://github.com/lepture/flask-shorturl', 10 | author='Hsiaoming Yang', 11 | author_email='me@lepture.com', 12 | description='Short URL generaotr for Flask', 13 | long_description=open('README.rst').read(), 14 | license=open('LICENSE').read(), 15 | py_modules=['flask_shorturl'], 16 | zip_safe=False, 17 | platforms='any', 18 | install_requires=['Flask'], 19 | tests_require=['nose'], 20 | test_suite='nose.collector', 21 | classifiers=[ 22 | 'Development Status :: 4 - Beta', 23 | 'Environment :: Web Environment', 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: BSD License', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python', 28 | 'Programming Language :: Python :: 2.6', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3.4', 31 | 'Programming Language :: Python :: 3.5', 32 | 'Programming Language :: Python :: Implementation', 33 | 'Programming Language :: Python :: Implementation :: CPython', 34 | 'Programming Language :: Python :: Implementation :: PyPy', 35 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 36 | 'Topic :: Software Development :: Libraries :: Python Modules' 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /test_shorturl.py: -------------------------------------------------------------------------------- 1 | from nose.tools import raises 2 | from flask import Flask 3 | from flask_shorturl import ShortUrl 4 | 5 | 6 | def test_shorturl(): 7 | app = Flask(__name__) 8 | 9 | su = ShortUrl(app) 10 | for a in range(0, 200000, 37): 11 | b = su.encode(a) 12 | c = su.enbase(b) 13 | d = su.debase(c) 14 | e = su.decode(d) 15 | assert a == e 16 | assert b == d 17 | assert c == su.encode_url(a) 18 | assert a == su.decode_url(c) 19 | 20 | 21 | def test_init_app(): 22 | app = Flask(__name__) 23 | su = ShortUrl() 24 | su.init_app(app) 25 | 26 | a = 21 27 | b = su.encode(a) 28 | c = su.enbase(b) 29 | d = su.debase(c) 30 | e = su.decode(d) 31 | assert a == e 32 | assert b == d 33 | assert c == su.encode_url(a) 34 | assert a == su.decode_url(c) 35 | 36 | 37 | def test_flask_ctx(): 38 | app = Flask(__name__) 39 | DEFAULT_ALPHABET = 'mn6j2c4rv8bpygw95z7hsdaetxuk3fq' 40 | app.config.setdefault('SHORT_URL_ALPHABET', DEFAULT_ALPHABET) 41 | app.config.setdefault('SHORT_URL_MIN_LENGTH', 5) 42 | app.config.setdefault('SHORT_URL_BLOCK_SIZE', 24) 43 | 44 | su = ShortUrl() 45 | 46 | with app.test_request_context(): 47 | a = 21 48 | b = su.encode(a) 49 | c = su.enbase(b) 50 | d = su.debase(c) 51 | e = su.decode(d) 52 | assert a == e 53 | assert b == d 54 | assert c == su.encode_url(a) 55 | assert a == su.decode_url(c) 56 | 57 | 58 | @raises(RuntimeError) 59 | def test_no_ctx(): 60 | su = ShortUrl() 61 | su.encode(21) 62 | --------------------------------------------------------------------------------