diff --git a/ipfshttpclient/client/__init__.py b/ipfshttpclient/client/__init__.py index 8b170103..65a56dc3 100644 --- a/ipfshttpclient/client/__init__.py +++ b/ipfshttpclient/client/__init__.py @@ -22,7 +22,7 @@ from . import block from . import bootstrap from . import config -#TODO: `from . import dag` +from . import dag from . import dht from . import files from . import key @@ -175,6 +175,7 @@ def close(self): # Call this when you're done block = base.SectionProperty(block.Section) bootstrap = base.SectionProperty(bootstrap.Section) config = base.SectionProperty(config.Section) + dag = base.SectionProperty(dag.Section) dht = base.SectionProperty(dht.Section) key = base.SectionProperty(key.Section) name = base.SectionProperty(name.Section) diff --git a/ipfshttpclient/client/dag.py b/ipfshttpclient/client/dag.py new file mode 100644 index 00000000..715b8d7f --- /dev/null +++ b/ipfshttpclient/client/dag.py @@ -0,0 +1,154 @@ +from . import base + +from .. import multipart +from .. import utils + + +class Section(base.SectionBase): + @base.returns_single_item(base.ResponseBase) + def get(self, cid: base.cid_t, **kwargs: base.CommonArgs): + """Retrieves the contents of a DAG node + + .. code-block:: python + + >>> client.dag.get('QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D') + {'Data': '\x08\x01', + 'Links': [ + {'Hash': 'Qmd2xkBfEwEs9oMTk77A6jrsgurpF3ugXSg7dtPNFkcNMV', + 'Name': 'Makefile', 'Size': 174}, + {'Hash': 'QmeKozNssnkJ4NcyRidYgDY2jfRZqVEoRGfipkgath71bX', + 'Name': 'example', 'Size': 1474}, + {'Hash': 'QmZAL3oHMQYqsV61tGvoAVtQLs1WzRe1zkkamv9qxqnDuK', + 'Name': 'home', 'Size': 3947}, + {'Hash': 'QmZNPyKVriMsZwJSNXeQtVQSNU4v4KEKGUQaMT61LPahso', + 'Name': 'lib', 'Size': 268261}, + {'Hash': 'QmSY8RfVntt3VdxWppv9w5hWgNrE31uctgTiYwKir8eXJY', + 'Name': 'published-version', 'Size': 55} + ]} + + Parameters + ---------- + cid + Key of the object to retrieve, in CID format + + Returns + ------- + dict + Cid with the address of the dag object + """ + args = (str(cid),) + return self._client.request('/dag/get', args, decoder='json', **kwargs) + + @base.returns_single_item(base.ResponseBase) + def put(self, data: utils.clean_file_t, **kwargs: base.CommonArgs): + """Decodes the given input file as a DAG object and returns their key + + .. code-block:: python + + >>> client.dag.put(io.BytesIO(b''' + ... { + ... "Data": "another", + ... "Links": [ { + ... "Name": "some link", + ... "Hash": "QmXg9Pp2ytZ14xgmQjYEiHjVjMFXzCV … R39V", + ... "Size": 8 + ... } ] + ... }''')) + {'Cid': { + '/': 'bafyreifgjgbmtykld2e3yncey3naek5xad3h4m2pxmo3of376qxh54qk34' + } + } + + Parameters + ---------- + data + IO stream object of path to a file containing the data to put + + Returns + ------- + dict + Cid with the address of the dag object + """ + body, headers = multipart.stream_files(data, chunk_size=self.chunk_size) + return self._client.request('/dag/put', decoder='json', data=body, + headers=headers, **kwargs) + + @base.returns_single_item(base.ResponseBase) + def resolve(self, cid: base.cid_t, **kwargs: base.CommonArgs): + """Resolves a DAG node from its CID, returning its address and remaining path + + .. code-block:: python + + >>> client.dag.resolve('QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D') + {'Cid': { + '/': 'QmTkzDwWqPbnAh5YiV5VwcTLnGdwSNsNTn2aDxdXBFca7D' + } + } + + Parameters + ---------- + cid + Key of the object to resolve, in CID format + + Returns + ------- + dict + Cid with the address of the dag object + """ + args = (str(cid),) + return self._client.request('/dag/resolve', args, decoder='json', **kwargs) + + @base.returns_single_item(base.ResponseBase) + def imprt(self, data: utils.clean_file_t, **kwargs: base.CommonArgs): + """Imports a .car file with a DAG into IPFS + + .. code-block:: python + + >>> with open('data.car', 'rb') as file + ... client.dag.imprt(file) + {'Root': { + 'Cid': { + '/': 'bafyreidepjmjhvhlvp5eyxqpmyyi7rxwvl7wsglwai3cnvq63komq4tdya' + } + } + } + + *Note*: This method is named ``.imprt`` (rather than ``.import``) to avoid causing a Python + :exc:`SyntaxError` due to ``import`` being global keyword in Python. + + Parameters + ---------- + data + IO stream object with data that should be imported + + Returns + ------- + dict + Dictionary with the root CID of the DAG imported + """ + body, headers = multipart.stream_files(data, chunk_size=self.chunk_size) + return self._client.request('/dag/import', decoder='json', data=body, + headers=headers, **kwargs) + + def export(self, cid: str, **kwargs: base.CommonArgs): + """Exports a DAG into a .car file format + + .. code-block:: python + + >>> data = client.dag.export('bafyreidepjmjhvhlvp5eyxqpmyyi7rxwvl7wsglwai3cnvq63komq4tdya') + + *Note*: When exporting larger DAG structures, remember that you can set the *stream* + parameter to ``True`` on any method to have it return results incrementally. + + Parameters + ---------- + cid + Key of the object to export, in CID format + + Returns + ------- + bytes + DAG in a .car format + """ + args = (str(cid),) + return self._client.request('/dag/export', args, **kwargs) diff --git a/test/functional/fake_json/data.car b/test/functional/fake_json/data.car new file mode 100644 index 00000000..af2b2e41 Binary files /dev/null and b/test/functional/fake_json/data.car differ diff --git a/test/functional/test_dag.py b/test/functional/test_dag.py new file mode 100644 index 00000000..7a5079e6 --- /dev/null +++ b/test/functional/test_dag.py @@ -0,0 +1,54 @@ +import io + +import pytest + +import conftest + + +def test_put_get_resolve(client): + version = tuple(map(int, client.version()["Version"].split('-', 1)[0].split('.'))) + if version < (0, 5): + pytest.skip("IPFS DAG APIs first appeared in go-IPFS 0.5") + + data = io.BytesIO(br'{"links": []}') + response = client.dag.put(data) + + assert 'Cid' in response + assert '/' in response['Cid'] + assert response['Cid']['/'] == 'bafyreidepjmjhvhlvp5eyxqpmyyi7rxwvl7wsglwai3cnvq63komq4tdya' + + response = client.dag.get('bafyreidepjmjhvhlvp5eyxqpmyyi7rxwvl7wsglwai3cnvq63komq4tdya') + + assert 'links' in response + assert response['links'] == [] + + response = client.dag.resolve('bafyreidepjmjhvhlvp5eyxqpmyyi7rxwvl7wsglwai3cnvq63komq4tdya') + + assert 'Cid' in response + assert response['Cid']['/'] == 'bafyreidepjmjhvhlvp5eyxqpmyyi7rxwvl7wsglwai3cnvq63komq4tdya' + + +def test_import_export(client): + version = tuple(map(int, client.version()["Version"].split('-', 1)[0].split('.'))) + if version < (0, 5): + pytest.skip("IPFS DAG APIs first appeared in go-IPFS 0.5") + + # This file was created by inserting a simple JSON object into IPFS and + # exporting it using `ipfs dag export > file.car` + data_car = conftest.TEST_DIR / 'fake_json' / 'data.car' + data_car = str(data_car) #PY35 + + with open(data_car, 'rb') as file: + response = client.dag.imprt(file) + + assert 'Root' in response + assert 'Cid' in response['Root'] + assert '/' in response['Root']['Cid'] + + cid = response['Root']['Cid'] + assert cid['/'] == 'bafyreidepjmjhvhlvp5eyxqpmyyi7rxwvl7wsglwai3cnvq63komq4tdya' + + data = client.dag.export('bafyreidepjmjhvhlvp5eyxqpmyyi7rxwvl7wsglwai3cnvq63komq4tdya') + + with open(data_car, 'rb') as file: + assert data == file.read()