From 97e3917a7fd08512e9c3eb2e78bd10bbadbb8766 Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie Date: Tue, 3 Aug 2021 08:42:36 -0600 Subject: [PATCH] Bring in a (bugged) proquint --- projects/proquint/README.md | 3 + projects/proquint/proquint.py | 105 ++++++++++++++++++++++++++++++++++ projects/proquint/setup.cfg | 13 +++++ projects/proquint/setup.py | 70 +++++++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 projects/proquint/README.md create mode 100644 projects/proquint/proquint.py create mode 100644 projects/proquint/setup.cfg create mode 100644 projects/proquint/setup.py diff --git a/projects/proquint/README.md b/projects/proquint/README.md new file mode 100644 index 0000000..f7fea94 --- /dev/null +++ b/projects/proquint/README.md @@ -0,0 +1,3 @@ +# Proquint + +An alternative implementation to https://github.com/dsw/proquint/tree/master/python, which is kinda garbo. diff --git a/projects/proquint/proquint.py b/projects/proquint/proquint.py new file mode 100644 index 0000000..45f9319 --- /dev/null +++ b/projects/proquint/proquint.py @@ -0,0 +1,105 @@ +"""Proquint - pronounceable codings of integers. + +Implemented from http://arxiv.org/html/0901.4016 +""" + +from functools import cache + + +class Proquint(object): + # Class parameters + ################################################################################################ + CONSONANTS = "bdfghjklmnprstvz" + VOWELS = "aiou" + BYTEORDER = "big" + + # Implementation helpers + ################################################################################################ + @classmethod + @cache + def _consonant_to_uint(cls, c: str) -> int: + if idx := cls.CONSONANTS.index(c) == -1: + raise KeyError + return idx + + @classmethod + @cache + def _vowel_to_uint(cls, c: str) -> int: + if idx := cls.VOWELS.index(c) == -1: + raise KeyError + return idx + + @classmethod + def _encode(cls, buffer: bytes) -> str: + for n, m in zip(buffer[0::2], buffer[1::2]): + n = n << 16 | m + c1 = n & 0x0F + v1 = (n >> 4) & 0x03 + c2 = (n >> 6) & 0x0F + v2 = (n >> 10) & 0x03 + c3 = (n >> 12) & 0x0F + + yield f"{cls.CONSONANTS[c1]}{cls.VOWELS[v1]}{cls.CONSONANTS[c2]}{cls.VOWELS[v2]}{cls.CONSONANTS[c3]}" + + # Core methods + ################################################################################################ + @classmethod + def encode_bytes(cls, buffer: bytes) -> str: + """Encode a sequence of bytes into a proquint string. + + >>> + """ + + return "-".join(cls._encode(buffer)) + + @classmethod + def decode(cls, buffer: str) -> int: + """Convert proquint string identifier into corresponding 32-bit integer value. + + >>> hex(Proquint.decode('lusab-babad')) + '0x7F000001' + """ + + res = 0 + + for i, c in enumerate([c for c in buffer if c != '-']): + if mag := cls._consonant_to_uint(c) is not None: + res <<= 4 + res += mag + else: + mag = cls._vowel_to_uint(c) + if mag is not None: + res <<= 2 + res += mag + elif i != 5: + raise ValueError('Bad proquint format') + return res + + # Handy aliases + ################################################################################################ + @classmethod + def encode(cls, val: int, width: int, byteorder=BYTEORDER): + """Encode an integer into a proquint string.""" + + if width % 8 != 0 or width < 8: + raise ValueError(f"Width must be a positive power of 2 greater than 8") + + return cls.encode_bytes(val.to_bytes(width // 8, byteorder)) + + @classmethod + def encode_i16(cls, val: int): + """Encode a 16bi int to a proquint string.""" + + return cls.encode(val, 16) + + @classmethod + def encode_i32(cls, val: int): + """Encode a 32bi int to a proquint string.""" + + return cls.encode(val, 32) + + @classmethod + def encode_i64(cls, val: int): + """Encode a 64bi int into a proquint string.""" + + return cls.encode(val, 64) diff --git a/projects/proquint/setup.cfg b/projects/proquint/setup.cfg new file mode 100644 index 0000000..bb3303f --- /dev/null +++ b/projects/proquint/setup.cfg @@ -0,0 +1,13 @@ +[metadata] +# This includes the license file(s) in the wheel. +# https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file +license_files = LICENSE.txt + +[bdist_wheel] +# This flag says to generate wheels that support both Python 2 and Python +# 3. If your code will not run unchanged on both Python 2 and 3, you will +# need to generate separate wheels for each Python version that you +# support. Removing this line (or setting universal to 0) will prevent +# bdist_wheel from trying to make a universal wheel. For more see: +# https://packaging.python.org/guides/distributing-packages-using-setuptools/#wheels +universal=1 diff --git a/projects/proquint/setup.py b/projects/proquint/setup.py new file mode 100644 index 0000000..d852561 --- /dev/null +++ b/projects/proquint/setup.py @@ -0,0 +1,70 @@ +"""A setuptools based setup module. + +""" + +# io.open is needed for projects that support Python 2.7 +# It ensures open() defaults to text mode with universal newlines, +# and accepts an argument to specify the text encoding +# Python 3 only projects can skip this import +from io import open +from os import path + +# Always prefer setuptools over distutils +from setuptools import find_packages, setup + + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, "README.md"), encoding="utf-8") as f: + long_description = f.read() + +# Arguments marked as "Required" below must be included for upload to PyPI. +# Fields marked as "Optional" may be commented out. + +setup( + name="proquint", # Required + version="0.1.0", # Required + description="Enunciable numerics", + long_description=long_description, # Optional + long_description_content_type="text/markdown", # Optional (see note above) + url="https://github.com/arrdem/source", + author="Reid 'arrdem' McKenzie", + author_email="me@arrdem.com", + classifiers=[ + # Optional + # https://pypi.org/pypi?%3Aaction=list_classifiers + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3.5", + ], + # This field adds keywords for your project which will appear on the + # project page. What does your project relate to? + # + # Note that this is a string of words separated by whitespace, not a list. + keywords="sample setuptools development", # Optional + # You can just specify package directories manually here if your project is + # simple. Or you can use find_packages(). + # + # Alternatively, if you just want to distribute a single Python file, use + # the `py_modules` argument instead as follows, which will expect a file + # called `my_module.py` to exist: + # + # py_modules=["my_module"], + # + packages=find_packages(exclude=["docs", "tests"]), + python_requires=">=3.5", + # List additional groups of dependencies here (e.g. development + # dependencies). Users will be able to install these using the "extras" + # syntax, for example: + # + # $ pip install sampleproject[dev] + # + # Similar to `install_requires` above, these must be valid existing + # projects. + extras_require={ # Optional + "dev": ["check-manifest"], + "test": ["pytest", "hypothesis"], + }, +)