├── tests ├── __init__.py └── test_ipld.py ├── pytest.ini ├── ipld ├── __init__.py └── ipld.py ├── .travis.yml ├── codecov.yml ├── LICENSE.md ├── .gitignore ├── README.rst └── setup.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | norecursedirs = .* *.egg *.egg-info env* devenv* docs 4 | -------------------------------------------------------------------------------- /ipld/__init__.py: -------------------------------------------------------------------------------- 1 | from ipld.ipld import LINK_TAG, LINK_SYMBOL, marshal, multihash, unmarshal 2 | 3 | __all__ = ['LINK_TAG', 'LINK_SYMBOL', 'marshal', 'multihash', 'unmarshal'] 4 | 5 | __version__ = '0.0.1' 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.4 4 | - 3.5 5 | install: 6 | - pip install codecov 7 | - pip install --process-dependency-links -e .[test] 8 | script: py.test -v --cov ipld 9 | after_success: codecov 10 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | bot: TimDaub 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70...100" 9 | 10 | status: 11 | project: 12 | default: 13 | target: auto 14 | if_no_uploads: error 15 | 16 | patch: 17 | default: 18 | target: "80%" 19 | if_no_uploads: error 20 | 21 | ignore: 22 | - "tests/*" 23 | 24 | comment: 25 | layout: "header, diff, changes, sunburst, uncovered" 26 | behavior: default 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 BigchainDB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/ipld.svg 2 | :target: https://pypi.python.org/pypi/ipld 3 | .. image:: https://img.shields.io/travis/bigchaindb/py-ipld.svg 4 | :target: https://travis-ci.org/bigchaindb/py-ipld 5 | .. image:: https://img.shields.io/codecov/c/github/bigchaindb/py-ipld/master.svg 6 | :target: https://codecov.io/github/bigchaindb/py-ipld?branch=master 7 | 8 | 9 | py-ipld 10 | ======= 11 | | Python implementation of the `IPLD specification `_. 12 | 13 | 14 | Status 15 | ------ 16 | For TODOs, see: `#1 ` 17 | 18 | 19 | Installation 20 | ------------ 21 | 22 | .. code-block:: bash 23 | 24 | $ pip install ipld 25 | 26 | 27 | Usage 28 | ----- 29 | In the Python REPL: 30 | 31 | .. code-block:: python 32 | 33 | >>> from ipld import marshal, multihash, unmarshal 34 | >>> 35 | >>> file = { 36 | ... 'name': 'hello.txt', 37 | ... 'size': 11 38 | ... } 39 | >>> 40 | >>> marshalled = marshal(file) 41 | >>> 42 | >>> multihash(marshalled) 43 | 'QmQtX5JVbRa25LmQ1LHFChkXWW5GaWrp7JpymN4oPuBSmL' 44 | >>> 45 | >>> unmarshal(marshal(file)) == file 46 | True 47 | 48 | That's it. No readthedocs, no private methods :boom:. 49 | 50 | 51 | Tests 52 | ----- 53 | *Only relevant, if you want to help developing.* 54 | 55 | .. code-block:: bash 56 | 57 | $ pip install -e .[dev] 58 | $ py.test -v 59 | 60 | 61 | Acknowledgements 62 | ---------------- 63 | Thanks to the `contributors `_ 64 | over at BigchainDB for letting me take their setup structure. 65 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from setuptools import setup, find_packages 5 | 6 | 7 | def read(*names, **kwargs): 8 | with open( 9 | os.path.join(os.path.dirname(__file__), *names), 10 | encoding=kwargs.get('encoding', 'utf8') 11 | ) as fp: 12 | return fp.read() 13 | 14 | 15 | def find_version(*file_paths): 16 | version_file = read(*file_paths) 17 | version_match = re.search( 18 | r'^__version__ = [\'"]([^\'"]*)[\'"]', version_file, re.M) 19 | if version_match: 20 | return version_match.group(1) 21 | raise RuntimeError('Unable to find version string.') 22 | 23 | 24 | with open('README.rst', encoding='utf-8') as readme: 25 | long_description = readme.read() 26 | 27 | 28 | tests_require = [ 29 | 'pep8', 30 | 'pyflakes', 31 | 'pylint', 32 | 'pytest', 33 | 'pytest-cov', 34 | 'pytest-xdist', 35 | 'pytest-flask', 36 | ] 37 | 38 | dev_require = [ 39 | 'ipdb', 40 | 'ipython', 41 | ] 42 | 43 | setup( 44 | name="ipld", 45 | packages=find_packages(exclude=['tests*']), 46 | version=find_version('ipld', '__init__.py'), 47 | author="Tim Daubenschuetz", 48 | author_email="tim.daubenschuetz@gmail.com", 49 | description="An IPLD implementation in Python", 50 | long_description=long_description, 51 | license='MIT License', 52 | keywords="ipld python ipfs bigchaindb", 53 | classifiers=[ 54 | 'Development Status :: 3 - Alpha', 55 | 'Intended Audience :: Developers', 56 | 'Natural Language :: English', 57 | 'License :: OSI Approved :: MIT License', 58 | 'Programming Language :: Python :: 3 :: Only', 59 | 'Programming Language :: Python :: 3.4', 60 | 'Programming Language :: Python :: 3.5', 61 | 'Topic :: Software Development', 62 | ], 63 | install_requires=[ 64 | 'cbor==1.0.0', 65 | 'base58==0.2.2', 66 | 'pymultihash==0.8.2', 67 | 'multiaddr==0.0.2', 68 | ], 69 | setup_requires=['pytest-runner'], 70 | tests_require=tests_require, 71 | extras_require={ 72 | 'test': tests_require, 73 | 'dev': dev_require + tests_require, 74 | }, 75 | ) 76 | -------------------------------------------------------------------------------- /ipld/ipld.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from cbor import dumps, loads, Tag 4 | from multiaddr import Multiaddr 5 | from multihash import digest 6 | 7 | 8 | # NOTE: jbenet plans to register tag 258: 9 | # https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml 10 | LINK_TAG = 258 11 | LINK_SYMBOL = '/' 12 | 13 | 14 | def marshal(json_data): 15 | cp_json_data = deepcopy(json_data) 16 | 17 | def transform(di): 18 | for k, v in di.items(): 19 | if isinstance(v, dict): 20 | di[k] = transform(v) 21 | 22 | if LINK_SYMBOL in di: 23 | link = di.pop(LINK_SYMBOL) 24 | 25 | try: 26 | link = Multiaddr(link).to_bytes() 27 | except ValueError: 28 | pass 29 | 30 | if di: 31 | raise KeyError('Links must not have siblings') 32 | return Tag(LINK_TAG, link) 33 | return di 34 | 35 | # TODO: Currently, all keys are being sorted. It is yet to be 36 | # determined if this is compatible with other implementations. 37 | return dumps(transform(cp_json_data), sort_keys=True) 38 | 39 | 40 | def unmarshal(cbor_data): 41 | json_data = loads(cbor_data) 42 | 43 | def transform(di): 44 | for k, v in di.items(): 45 | if isinstance(v, Tag): 46 | link = Multiaddr(bytes_addr=v.value) 47 | 48 | try: 49 | # the __str__ method of Multiaddr could fail 50 | # if wrong values are passed to it (for example, 51 | # any value that is not a bytes list) 52 | link = str(link) 53 | except: 54 | link = v.value 55 | finally: 56 | di[k] = { 57 | LINK_SYMBOL: link 58 | } 59 | 60 | elif isinstance(v, dict): 61 | di[k] = transform(v) 62 | return di 63 | return transform(json_data) 64 | 65 | 66 | def multihash(data, fn_name='sha2_256'): 67 | """A utility function to make multihashing more convenient 68 | 69 | Args: 70 | data (bytes str): Any Python dict that is an output of the 71 | `marshal` function 72 | fn_name (str): Any of the following string values: 'sha1', 73 | 'sha2_256', 'sha2_512', 'sha3_512', 'sha3', 'sha3_384', 74 | 'sha3_256', 'sha3_224', 'shake_128', 'shake_256', 'blake2b', 75 | 'blake2s' 76 | 77 | Returns: 78 | A base58 encoded digest of a hash (encoded in ascii) 79 | 80 | """ 81 | return digest(data, fn_name).encode('base58').decode('ascii') 82 | -------------------------------------------------------------------------------- /tests/test_ipld.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cbor import dumps, Tag 4 | from ipld import LINK_TAG, marshal, multihash, unmarshal 5 | from multiaddr import Multiaddr 6 | 7 | 8 | def test_transform_dict_to_cbor(): 9 | src = { 10 | 'hello': 'world', 11 | 'num': 1, 12 | } 13 | 14 | expected = { 15 | 'num': 1, 16 | 'hello': 'world', 17 | } 18 | 19 | assert marshal(src) == dumps(expected, sort_keys=True) 20 | 21 | 22 | def test_transform_dict_with_link_to_cbor(): 23 | src = { 24 | 'hello': 'world', 25 | 'num': 1, 26 | 'l1': { 27 | '/': 'takemedowntotheparadisecity', 28 | }, 29 | } 30 | 31 | expected = { 32 | 'num': 1, 33 | 'hello': 'world', 34 | 'l1': Tag(LINK_TAG, 'takemedowntotheparadisecity'), 35 | } 36 | 37 | assert marshal(src) == dumps(expected, sort_keys=True) 38 | 39 | 40 | def test_transform_dict_with_nested_link_to_cbor(): 41 | src = { 42 | 'hello': 'world', 43 | 'num': 1, 44 | 'l1': { 45 | '/': 'takemedowntotheparadisecity', 46 | }, 47 | 'secret': { 48 | 'l1': { 49 | '/': 'Ihhh ein Sekret!', 50 | }, 51 | }, 52 | } 53 | 54 | expected = { 55 | 'num': 1, 56 | 'hello': 'world', 57 | 'l1': Tag(LINK_TAG, 'takemedowntotheparadisecity'), 58 | 'secret': { 59 | 'l1': Tag(LINK_TAG, 'Ihhh ein Sekret!') 60 | }, 61 | } 62 | 63 | assert marshal(src) == dumps(expected, sort_keys=True) 64 | 65 | 66 | def test_transformation_doesnt_mutate_input(): 67 | src = { 68 | 'l1': { 69 | '/': 'takemedowntotheparadisecity', 70 | }, 71 | } 72 | 73 | expected = { 74 | 'l1': { 75 | '/': 'takemedowntotheparadisecity', 76 | }, 77 | } 78 | 79 | marshal(src) 80 | 81 | assert src == expected 82 | 83 | 84 | def test_transform_raise_key_error(): 85 | src = { 86 | 'l1': { 87 | '/': 'takemedowntotheparadisecity', 88 | 'badlinksibling': 'icauseakeyerror', 89 | 'badlinksibling2': 'icauseakeyerror' 90 | }, 91 | } 92 | 93 | with pytest.raises(KeyError): 94 | marshal(src) 95 | 96 | 97 | def test_transform_cbor_to_dict(): 98 | src = dumps({ 99 | 'hello': 'world', 100 | 'num': 1, 101 | }, sort_keys=True) 102 | 103 | expected = { 104 | 'num': 1, 105 | 'hello': 'world', 106 | } 107 | 108 | assert unmarshal(src) == expected 109 | 110 | 111 | def test_transform_cbor_with_link_to_dict(): 112 | src = dumps({ 113 | 'num': 1, 114 | 'hello': 'world', 115 | 'l1': Tag(LINK_TAG, 'takemedowntotheparadisecity'), 116 | }, sort_keys=True) 117 | 118 | expected = { 119 | 'hello': 'world', 120 | 'num': 1, 121 | 'l1': { 122 | '/': 'takemedowntotheparadisecity', 123 | }, 124 | } 125 | 126 | assert unmarshal(src) == expected 127 | 128 | 129 | def test_transform_cbor_with_nested_link_to_dict(): 130 | src = dumps({ 131 | 'num': 1, 132 | 'hello': 'world', 133 | 'l1': Tag(LINK_TAG, 'takemedowntotheparadisecity'), 134 | 'secret': { 135 | 'l1': Tag(LINK_TAG, 'Ihhh ein Sekret!') 136 | }, 137 | }, sort_keys=True) 138 | 139 | expected = { 140 | 'hello': 'world', 141 | 'num': 1, 142 | 'l1': { 143 | '/': 'takemedowntotheparadisecity', 144 | }, 145 | 'secret': { 146 | 'l1': { 147 | '/': 'Ihhh ein Sekret!', 148 | }, 149 | }, 150 | } 151 | 152 | assert unmarshal(src) == expected 153 | 154 | 155 | def test_transform_dict_to_cbor_with_multiaddr(): 156 | addr1 = Multiaddr('/ip4/127.0.0.1/udp/1234') 157 | addr2 = Multiaddr('/ipfs/Qmafmh1Cw3H1bwdYpaaj5AbCW4LkYyUWaM7Nykpn5NZoYL') 158 | 159 | src = { 160 | 'data': 'hello world', 161 | 'size': 11, 162 | 'l1': { 163 | '/': str(addr1), 164 | }, 165 | 'l2': { 166 | '/': str(addr2), 167 | } 168 | } 169 | 170 | expected = { 171 | 'data': 'hello world', 172 | 'size': 11, 173 | 'l1': Tag(LINK_TAG, addr1.to_bytes()), 174 | 'l2': Tag(LINK_TAG, addr2.to_bytes()), 175 | } 176 | 177 | assert marshal(src) == dumps(expected, sort_keys=True) 178 | 179 | 180 | def test_transform_cbor_to_dict_with_multiaddr(): 181 | addr1 = Multiaddr('/ip4/127.0.0.1/udp/1234') 182 | addr2 = Multiaddr('/ipfs/Qmafmh1Cw3H1bwdYpaaj5AbCW4LkYyUWaM7Nykpn5NZoYL') 183 | 184 | src = dumps({ 185 | 'data': 'hello world', 186 | 'size': 11, 187 | 'l1': Tag(LINK_TAG, addr1.to_bytes()), 188 | 'l2': Tag(LINK_TAG, addr2.to_bytes()), 189 | }, sort_keys=True) 190 | 191 | expected = { 192 | 'data': 'hello world', 193 | 'size': 11, 194 | 'l1': { 195 | '/': str(addr1), 196 | }, 197 | 'l2': { 198 | '/': str(addr2), 199 | } 200 | } 201 | 202 | assert unmarshal(src) == expected 203 | 204 | 205 | def test_multihashing_cbor(): 206 | src = dumps({ 207 | 'name': 'hello.txt', 208 | 'size': 11 209 | }, sort_keys=True) 210 | 211 | assert multihash(src) == 'QmQtX5JVbRa25LmQ1LHFChkXWW5GaWrp7JpymN4oPuBSmL' 212 | --------------------------------------------------------------------------------