Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
5767539
Add Coveralls badge
suminb Mar 29, 2018
f1ed369
Add an assertion statement
suminb Jun 5, 2018
9a542ed
Test for invalid alphabets
suminb Jun 5, 2018
f29e401
Test case for invalid byte string
suminb Jun 5, 2018
3780da8
Fix pytest version to 3.6.1
suminb Jun 5, 2018
f09cadf
Drop support for Python 2.6
suminb Jun 5, 2018
15950a0
Update version to 0.3.3
suminb Jul 30, 2018
d07e26a
Merge branch 'release/0.3.3'
suminb Jul 30, 2018
732465d
Merge branch 'release/0.3.3' into develop
suminb Jul 30, 2018
1bb3a24
Update README
suminb Jul 30, 2018
53ef1c5
Use an inverted charset to integrate with external code
joelnb Jun 26, 2018
44331f7
Support both types of charset
joelnb Jun 26, 2018
ab786ae
Merge pull request #13 from joelnb/allow-inverted-charset
suminb Jul 31, 2018
68bb47d
Merge branch 'release/0.4.0'
suminb Jul 31, 2018
b6a2403
Merge tag '0.4.0' into develop
suminb Jul 31, 2018
082f510
Setting up SonarCloud
suminb Jun 27, 2019
dcaaf28
Advertise supported Python versions
suminb Jul 25, 2019
6821b40
Merge pull request #16 from suminb/feature/advertise-python-versions
suminb Jul 25, 2019
385ff3b
Update version (0.4.0 -> 0.4.1)
suminb Jul 26, 2019
cd52f77
Merge branch 'release/0.4.1'
suminb Jul 26, 2019
fa50b76
Merge branch 'release/0.4.1' into develop
suminb Jul 26, 2019
31e9217
Use plural forms for RESTful URLs
suminb Sep 11, 2019
db64549
Support latest/future Python versions
suminb Sep 11, 2019
f46622d
Remove `dist` key
suminb Sep 11, 2019
a9b9e7e
Remove older versions of Python
suminb Sep 27, 2019
cfae1e2
Make sure the return type of encodebytes() is string
suminb Oct 9, 2019
4f72c49
Make sure the return type of decodebytes() is bytes
suminb Oct 9, 2019
e3fbcac
Revise type checking code
suminb Oct 9, 2019
808c970
Type checking for encodebytes(), decodebytes()
suminb Oct 9, 2019
f122193
Maintain backward compatibility with Python 2.7
suminb Oct 9, 2019
9e7e896
Use a meaningful variable name (s -> encoded)
suminb Oct 9, 2019
6e28166
Use a meaningful variable name (s -> barray)
suminb Oct 9, 2019
13aa8ee
Merge branch 'feature/type-checking' into develop
suminb Oct 9, 2019
a07c4d2
Update version info (0.4.1 -> 0.4.2)
suminb Oct 9, 2019
58b2bba
Minor revisions on README
suminb Oct 9, 2019
a05f346
Documentation on function commutativity
suminb Oct 9, 2019
1a54e0f
Merge branch 'release/0.4.2'
suminb Oct 9, 2019
a27bd1f
Merge branch 'release/0.4.2' into develop
suminb Oct 9, 2019
7751cce
Temporarily remove `math` directives
suminb Oct 9, 2019
8f3c707
Merge branch 'hotfix/remove-math' into develop
suminb Oct 9, 2019
5d24f8a
Update supported languages information
suminb Oct 9, 2019
52205a4
Support for PyPy
suminb Oct 11, 2019
9562045
Reformat code with Black
suminb Oct 29, 2019
5e09e26
Run Black command during build stage
suminb Oct 29, 2019
3f180b7
Run Black for Python 3.6 and above only
suminb Oct 29, 2019
ab60f8b
Merge branch 'feature/black' into develop
suminb Oct 29, 2019
6b78441
Update version info (0.4.2 -> 0.4.3)
suminb Oct 29, 2019
c0b853c
Update URL
suminb Oct 29, 2019
f555ef3
Merge branch 'release/0.4.3' into develop
suminb Oct 29, 2019
8ff106a
feat: Support encoding with arbitrary charsets
taion Jul 24, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
dist: trusty
sudo: false
language: python

python:
- "2.6"
- "2.7"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
# - "pypy" # disable pypy builds until supported by trusty containers
- "3.7"
- "3.8-dev"
- "pypy"

addons:
sonarcloud:
organization: "suminb-github"

install:
- pip install --requirement tests/requirements.txt
- pip install "black; python_version >= '3.6'"

script:
- |
if [ -x "$(command -v black)" ]; then
black --check .
fi
- py.test tests --cov base62 --durations=10

after_success:
- coveralls
- sonar-scanner
39 changes: 33 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
base62
======

|Build Status| |PyPI|
|Build Status| |Coveralls| |PyPI|

A Python module for ``base62`` encoding. Ported from PHP code that I wrote
in mid-2000, which can be found
`here <http://blog.suminb.com/archives/558>`__.
`here <http://philosophical.one/posts/base62>`__.

.. |Build Status| image:: https://travis-ci.org/suminb/base62.svg?branch=master
:target: https://travis-ci.org/suminb/base62
.. |PyPI| image:: https://img.shields.io/pypi/v/pybase62.svg
:target: https://pypi.python.org/pypi/pybase62
.. |Coveralls| image:: https://coveralls.io/repos/github/suminb/base62/badge.svg?branch=master
:target: https://coveralls.io/github/suminb/base62?branch=develop


Rationale
---------

When writing a web application, often times we would like to keep the URLs short.
When writing a web application, often times we would like to keep the URLs
short.

::

http://localhost/post/V1Biicwt
http://localhost/posts/V1Biicwt

This certainly gives a more concise look than the following.

::

http://localhost/post/109237591284123
http://localhost/posts/109237591284123

This was the original motivation to write this module, but there shall be much
more broader potential use cases of this module. The main advantage of
Expand Down Expand Up @@ -84,6 +87,20 @@ From version ``0.2.0``, ``base62`` supports ``bytes`` array encoding as well.
>>> base62.decodebytes('1')
b'\x01'

Some may be inclined to assume that they both take ``bytes`` types as input
due to their namings. However, ``encodebytes()`` takes ``bytes`` types
whereas ``decodebytes()`` takes ``str`` types as an input. They are intended
to be commutative, so that a *roundtrip* between both functions yields the
original value.

Formally speaking, we say function *f* and *g* commute if *f∘g* = *g∘f* where
*f(g(x))* = *(f∘g)(x)*.

Therefore, we may expect the following relationships:

* ``value == encodebytes(decodebytes(value))``
* ``value == decodebytes(encodebytes(value))``

Tests
=====

Expand All @@ -93,8 +110,18 @@ You may run some test cases to ensure all functionalities are operational.

py.test -v

If ``pytest`` is not installed, you may want to run the following commands:
If ``pytest`` is not installed, you may want to run the following command:

::

pip install -r tests/requirements.txt


Deployment
==========

Deploy a source package (to `pypi <https://pypi.org>`_) as follows:

::

python setup.py sdist upload
108 changes: 66 additions & 42 deletions base62.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,109 +6,133 @@
Originated from http://blog.suminb.com/archives/558
"""

__title__ = 'base62'
__author__ = 'Sumin Byeon'
__email__ = 'suminb@gmail.com'
__version__ = '0.3.2'

CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
BASE = 62


def bytes_to_int(s, byteorder='big', signed=False):
__title__ = "base62"
__author__ = "Sumin Byeon"
__email__ = "suminb@gmail.com"
__version__ = "0.4.3"

CHARSET_DEFAULT = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
CHARSET_INVERTED = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

try:
# NOTE: This is for Python 2. Shall be removed as soon as Python 2 is
# deprecated.
string_types = (str, unicode)
bytes_types = (
bytes,
bytearray,
)
except NameError:
string_types = (str,)
bytes_types = (bytes,)


def bytes_to_int(barray, byteorder="big", signed=False):
"""Converts a byte array to an integer value.

Python 3 comes with a built-in function to do this, but we would like to
keep our code Python 2 compatible.
"""

try:
return int.from_bytes(s, byteorder, signed=signed)
return int.from_bytes(barray, byteorder, signed=signed)
except AttributeError:
# For Python 2.x
if byteorder != 'big' or signed:
if byteorder != "big" or signed:
raise NotImplementedError()

# NOTE: This won't work if a generator is given
n = len(s)
ds = (x << (8 * (n - 1 - i)) for i, x in enumerate(bytearray(s)))
n = len(barray)
ds = (x << (8 * (n - 1 - i)) for i, x in enumerate(bytearray(barray)))

return sum(ds)


def encode(n, minlen=1):
def encode(n, minlen=1, charset=CHARSET_DEFAULT):
"""Encodes a given integer ``n``."""
base = len(charset)

chs = []
while n > 0:
r = n % BASE
n //= BASE
r = n % base
n //= base

chs.append(CHARSET[r])
chs.append(charset[r])

if len(chs) > 0:
chs.reverse()
else:
chs.append('0')
chs.append("0")

s = ''.join(chs)
s = CHARSET[0] * max(minlen - len(s), 0) + s
s = "".join(chs)
s = charset[0] * max(minlen - len(s), 0) + s
return s


def encodebytes(s):
def encodebytes(barray, charset=CHARSET_DEFAULT):
"""Encodes a bytestring into a base62 string.

:param s: A byte array
:param barray: A byte array
:type barray: bytes
:rtype: str
"""

_check_bytes_type(s)
return encode(bytes_to_int(s))
_check_type(barray, bytes_types)
return encode(bytes_to_int(barray), charset=charset)


def decode(b):
"""Decodes a base62 encoded value ``b``."""
def decode(encoded, charset=CHARSET_DEFAULT):
"""Decodes a base62 encoded value ``encoded``.

:type encoded: str
:rtype: int
"""
_check_type(encoded, string_types)
base = len(charset)

if b.startswith('0z'):
b = b[2:]
if encoded.startswith("0z"):
encoded = encoded[2:]

l, i, v = len(b), 0, 0
for x in b:
v += _value(x) * (BASE ** (l - (i + 1)))
l, i, v = len(encoded), 0, 0
for x in encoded:
v += _value(x, charset=charset) * (BASE ** (l - (i + 1)))
i += 1

return v


def decodebytes(s):
def decodebytes(encoded, charset=CHARSET_DEFAULT):
"""Decodes a string of base62 data into a bytes object.

:param s: A string to be decoded in base62
:param encoded: A string to be decoded in base62
:type encoded: str
:rtype: bytes
"""

decoded = decode(s)
decoded = decode(encoded, charset=charset)
buf = bytearray()
while decoded > 0:
buf.append(decoded & 0xff)
buf.append(decoded & 0xFF)
decoded //= 256
buf.reverse()

return bytes(buf)


def _value(ch):
def _value(ch, charset):
"""Decodes an individual digit of a base62 encoded string."""

try:
return CHARSET.index(ch)
return charset.index(ch)
except ValueError:
raise ValueError('base62: Invalid character (%s)' % ch)
raise ValueError("base62: Invalid character (%s)" % ch)


def _check_bytes_type(s):
def _check_type(value, expected_type):
"""Checks if the input is in an appropriate type."""

if not isinstance(s, bytes):
msg = 'expected bytes-like object, not %s' % s.__class__.__name__
if not isinstance(value, expected_type):
msg = "Expected {} object, not {}".format(
expected_type, value.__class__.__name__
)
raise TypeError(msg)
31 changes: 19 additions & 12 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@

def readme():
try:
with open('README.rst') as f:
with open("README.rst") as f:
return f.read()
except:
return '(Could not read from README.rst)'
return "(Could not read from README.rst)"


setup(name='pybase62',
py_modules=['base62'],
version=base62.__version__,
description='Python module for base62 encoding',
long_description=readme(),
author='Sumin Byeon',
author_email='suminb@gmail.com',
url='http://github.com/suminb/base62',
packages=[],
)
setup(
name="pybase62",
py_modules=["base62"],
version=base62.__version__,
description="Python module for base62 encoding",
long_description=readme(),
author="Sumin Byeon",
author_email="suminb@gmail.com",
url="http://github.com/suminb/base62",
packages=[],
classifiers=[
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
],
)
4 changes: 4 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
sonar.projectKey=base62
sonar.sources=.
sonar.host.url=https://sonarcloud.io
sonar.login=travisci
2 changes: 1 addition & 1 deletion tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pytest
pytest==3.6.1
pytest-cov
coveralls
Loading