forked from LBRYCommunity/lbry-sdk
fef0cc764d
Removed the comment API Removed tests for the comment API Removed the documentation section Removed the comment server configuration
528 lines
16 KiB
Python
528 lines
16 KiB
Python
import os
|
|
import re
|
|
import json
|
|
import inspect
|
|
import tempfile
|
|
import asyncio
|
|
import time
|
|
from docopt import docopt
|
|
from binascii import unhexlify
|
|
from textwrap import indent
|
|
from lbry.testcase import CommandTestCase
|
|
from lbry.extras.cli import set_kwargs, get_argument_parser
|
|
from lbry.extras.daemon.daemon import (
|
|
Daemon, jsonrpc_dumps_pretty, encode_pagination_doc
|
|
)
|
|
from lbry.extras.daemon.json_response_encoder import (
|
|
encode_tx_doc, encode_txo_doc, encode_account_doc, encode_file_doc,
|
|
encode_wallet_doc
|
|
)
|
|
|
|
|
|
RETURN_DOCS = {
|
|
'Account': encode_account_doc(),
|
|
'Wallet': encode_wallet_doc(),
|
|
'File': encode_file_doc(),
|
|
'Transaction': encode_tx_doc(),
|
|
'Output': encode_txo_doc(),
|
|
'Address': 'an address in base58',
|
|
'Dict': 'glorious data in dictionary',
|
|
}
|
|
|
|
|
|
class ExampleRecorder:
|
|
def __init__(self, test):
|
|
self.test = test
|
|
self.examples = {}
|
|
|
|
async def __call__(self, title, *command):
|
|
parser = get_argument_parser()
|
|
args, command_args = parser.parse_known_args(command)
|
|
|
|
api_method_name = args.api_method_name
|
|
parsed = docopt(args.doc, command_args)
|
|
kwargs = set_kwargs(parsed)
|
|
for k, v in kwargs.items():
|
|
if v and isinstance(v, str) and (v[0], v[-1]) == ('"', '"'):
|
|
kwargs[k] = v[1:-1]
|
|
params = json.dumps({"method": api_method_name, "params": kwargs})
|
|
|
|
method = getattr(self.test.daemon, f'jsonrpc_{api_method_name}')
|
|
result = method(**kwargs)
|
|
if asyncio.iscoroutine(result):
|
|
result = await result
|
|
output = jsonrpc_dumps_pretty(result, ledger=self.test.daemon.ledger)
|
|
self.examples.setdefault(api_method_name, []).append({
|
|
'title': title,
|
|
'curl': f"curl -d'{params}' http://localhost:5279/",
|
|
'lbrynet': 'lbrynet ' + ' '.join(command),
|
|
'python': f'requests.post("http://localhost:5279", json={params}).json()',
|
|
'output': output.strip()
|
|
})
|
|
return json.loads(output)['result']
|
|
|
|
|
|
class Examples(CommandTestCase):
|
|
|
|
async def asyncSetUp(self):
|
|
await super().asyncSetUp()
|
|
self.recorder = ExampleRecorder(self)
|
|
|
|
async def play(self):
|
|
r = self.recorder
|
|
|
|
# general sdk
|
|
|
|
await r(
|
|
'Get status',
|
|
'status'
|
|
)
|
|
await r(
|
|
'Get version',
|
|
'version'
|
|
)
|
|
|
|
# settings
|
|
|
|
await r(
|
|
'Get settings',
|
|
'settings', 'get'
|
|
)
|
|
|
|
await r(
|
|
'Set settings',
|
|
'settings', 'set', '"tcp_port"', '99'
|
|
)
|
|
|
|
# preferences
|
|
|
|
await r(
|
|
'Set preference',
|
|
'preference', 'set', '"theme"', '"dark"'
|
|
)
|
|
|
|
await r(
|
|
'Get preferences',
|
|
'preference', 'get'
|
|
)
|
|
|
|
# wallets
|
|
|
|
await r(
|
|
'List your wallets',
|
|
'wallet', 'list'
|
|
)
|
|
|
|
# accounts
|
|
|
|
await r(
|
|
'List your accounts',
|
|
'account', 'list'
|
|
)
|
|
|
|
account = await r(
|
|
'Create an account',
|
|
'account', 'create', '"generated account"'
|
|
)
|
|
|
|
await r(
|
|
'Remove an account',
|
|
'account', 'remove', account['id']
|
|
)
|
|
|
|
await r(
|
|
'Add an account from seed',
|
|
'account', 'add', '"new account"', f"--seed=\"{account['seed']}\""
|
|
)
|
|
|
|
await r(
|
|
'Modify maximum number of times a change address can be reused',
|
|
'account', 'set', account['id'], '--change_max_uses=10'
|
|
)
|
|
|
|
# addresses
|
|
|
|
await r(
|
|
'List addresses in default account',
|
|
'address', 'list'
|
|
)
|
|
|
|
an_address = await r(
|
|
'Get an unused address',
|
|
'address', 'unused'
|
|
)
|
|
|
|
address_list_by_id = await r(
|
|
'List addresses in specified account',
|
|
'address', 'list', f"--account_id=\"{account['id']}\""
|
|
)
|
|
|
|
await r(
|
|
'Check if address is mine',
|
|
'address', 'is_mine', an_address
|
|
)
|
|
|
|
# sends/funds
|
|
|
|
transfer = await r(
|
|
'Transfer 2 LBC from default account to specific account',
|
|
'account', 'fund', f"--to_account=\"{account['id']}\"", "--amount=2.0", "--broadcast"
|
|
)
|
|
|
|
await self.on_transaction_dict(transfer)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(transfer)
|
|
|
|
await r(
|
|
'Get default account balance',
|
|
'account', 'balance'
|
|
)
|
|
|
|
txlist = await r(
|
|
'List your transactions',
|
|
'transaction', 'list'
|
|
)
|
|
|
|
await r(
|
|
'Get balance for specific account by id',
|
|
'account', 'balance', f"\"{account['id']}\""
|
|
)
|
|
|
|
spread_transaction = await r(
|
|
'Spread LBC between multiple addresses',
|
|
'account', 'fund', f"--to_account=\"{account['id']}\"", f"--from_account=\"{account['id']}\"", '--amount=1.5', '--outputs=2', '--broadcast'
|
|
)
|
|
|
|
await self.on_transaction_dict(spread_transaction)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(spread_transaction)
|
|
|
|
await r(
|
|
'Transfer all LBC to a specified account',
|
|
'account', 'fund', f"--from_account=\"{account['id']}\"", "--everything", "--broadcast"
|
|
)
|
|
|
|
# channels
|
|
|
|
channel = await r(
|
|
'Create a channel claim without metadata',
|
|
'channel', 'create', '@channel', '1.0'
|
|
)
|
|
channel_id = self.get_claim_id(channel)
|
|
await self.on_transaction_dict(channel)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(channel)
|
|
|
|
await r(
|
|
'List your channel claims',
|
|
'channel', 'list'
|
|
)
|
|
|
|
await r(
|
|
'Paginate your channel claims',
|
|
'channel', 'list', '--page=1', '--page_size=20'
|
|
)
|
|
|
|
channel = await r(
|
|
'Update a channel claim',
|
|
'channel', 'update', self.get_claim_id(channel), '--title="New Channel"'
|
|
)
|
|
|
|
await self.on_transaction_dict(channel)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(channel)
|
|
|
|
big_channel = await r(
|
|
'Create a channel claim with all metadata',
|
|
'channel', 'create', '@bigchannel', '1.0',
|
|
'--title="Big Channel"', '--description="A channel with lots of videos."',
|
|
'--email="creator@smallmedia.com"', '--tags=music', '--tags=art',
|
|
'--languages=pt-BR', '--languages=uk', '--locations=BR', '--locations=UA::Kiyv',
|
|
'--website_url="http://smallmedia.com"', '--thumbnail_url="http://smallmedia.com/logo.jpg"',
|
|
'--cover_url="http://smallmedia.com/logo.jpg"'
|
|
)
|
|
await self.on_transaction_dict(big_channel)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(big_channel)
|
|
await self.daemon.jsonrpc_channel_abandon(self.get_claim_id(big_channel))
|
|
await self.generate(1)
|
|
|
|
# stream claims
|
|
|
|
with tempfile.NamedTemporaryFile() as file:
|
|
file.write(b'hello world')
|
|
file.flush()
|
|
stream = await r(
|
|
'Create a stream claim without metadata',
|
|
'stream', 'create', 'astream', '1.0', file.name
|
|
)
|
|
await self.on_transaction_dict(stream)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(stream)
|
|
stream_id = self.get_claim_id(stream)
|
|
stream_name = stream['outputs'][0]['name']
|
|
stream = await r(
|
|
'Update a stream claim to add channel',
|
|
'stream', 'update', stream_id,
|
|
f'--channel_id="{channel_id}"'
|
|
)
|
|
await self.on_transaction_dict(stream)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(stream)
|
|
|
|
await r(
|
|
'List all your claims',
|
|
'claim', 'list'
|
|
)
|
|
|
|
await r(
|
|
'Paginate your claims',
|
|
'claim', 'list', '--page=1', '--page_size=20'
|
|
)
|
|
|
|
await r(
|
|
'List all your stream claims',
|
|
'stream', 'list'
|
|
)
|
|
|
|
await r(
|
|
'Paginate your stream claims',
|
|
'stream', 'list', '--page=1', '--page_size=20'
|
|
)
|
|
|
|
await r(
|
|
'Search for all claims in channel',
|
|
'claim', 'search', '--channel=@channel'
|
|
)
|
|
|
|
await r(
|
|
'Search for claims matching a name',
|
|
'claim', 'search', f'--name="{stream_name}"'
|
|
)
|
|
|
|
with tempfile.NamedTemporaryFile(suffix='.png') as file:
|
|
file.write(unhexlify(
|
|
b'89504e470d0a1a0a0000000d49484452000000050000000708020000004fc'
|
|
b'510b9000000097048597300000b1300000b1301009a9c1800000015494441'
|
|
b'5408d763fcffff3f031260624005d4e603004c45030b5286e9ea000000004'
|
|
b'9454e44ae426082'
|
|
))
|
|
file.flush()
|
|
big_stream = await r(
|
|
'Create an image stream claim with all metadata and fee',
|
|
'stream', 'create', 'blank-image', '1.0', file.name,
|
|
'--tags=blank', '--tags=art', '--languages=en', '--locations=US:NH:Manchester',
|
|
'--fee_currency=LBC', '--fee_amount=0.3',
|
|
'--title="Blank Image"', '--description="A blank PNG that is 5x7."', '--author=Picaso',
|
|
'--license="Public Domain"', '--license_url=http://public-domain.org',
|
|
'--thumbnail_url="http://smallmedia.com/thumbnail.jpg"', f'--release_time={int(time.time())}',
|
|
f'--channel_id="{channel_id}"'
|
|
)
|
|
await self.on_transaction_dict(big_stream)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(big_stream)
|
|
|
|
await self.daemon.jsonrpc_channel_abandon(self.get_claim_id(big_stream))
|
|
await self.generate(1)
|
|
|
|
# collections
|
|
collection = await r(
|
|
'Create a collection of one stream',
|
|
'collection', 'create',
|
|
'--name=tom', '--bid=1.0',
|
|
f'--channel_id={channel_id}',
|
|
f'--claims={stream_id}'
|
|
)
|
|
|
|
await self.on_transaction_dict(collection)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(collection)
|
|
|
|
await r(
|
|
'List collections',
|
|
'collection', 'list',
|
|
'--resolve', '--resolve_claims=1',
|
|
)
|
|
|
|
|
|
# files
|
|
|
|
file_list_result = (await r(
|
|
'List local files',
|
|
'file', 'list'
|
|
))['items']
|
|
file_uri = f"{file_list_result[0]['claim_name']}#{file_list_result[0]['claim_id']}"
|
|
await r(
|
|
'Resolve a claim',
|
|
'resolve', file_uri
|
|
)
|
|
|
|
await r(
|
|
'List files matching a parameter',
|
|
'file', 'list', f"--claim_id=\"{file_list_result[0]['claim_id']}\""
|
|
)
|
|
|
|
await r(
|
|
'Delete a file',
|
|
'file', 'delete', f"--claim_id=\"{file_list_result[0]['claim_id']}\""
|
|
)
|
|
|
|
await r(
|
|
'Get a file',
|
|
'get', file_uri
|
|
)
|
|
|
|
await r(
|
|
'Save a file to the downloads directory',
|
|
'file', 'save', f"--sd_hash=\"{file_list_result[0]['sd_hash']}\""
|
|
)
|
|
|
|
# blobs
|
|
|
|
bloblist = await r(
|
|
'List your local blobs',
|
|
'blob', 'list'
|
|
)
|
|
|
|
await r(
|
|
'Delete a blob',
|
|
'blob', 'delete', f"{bloblist['items'][0]}"
|
|
)
|
|
|
|
# abandon all the things
|
|
|
|
abandon_stream = await r(
|
|
'Abandon a stream claim',
|
|
'stream', 'abandon', stream_id
|
|
)
|
|
await self.on_transaction_dict(abandon_stream)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(abandon_stream)
|
|
|
|
abandon_channel = await r(
|
|
'Abandon a channel claim',
|
|
'channel', 'abandon', channel_id
|
|
)
|
|
await self.on_transaction_dict(abandon_channel)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(abandon_channel)
|
|
|
|
with tempfile.NamedTemporaryFile() as file:
|
|
file.write(b'hello world')
|
|
file.flush()
|
|
stream = await r(
|
|
'Publish a file',
|
|
'publish', 'a-new-stream', '--bid=1.0', f'--file_path={file.name}'
|
|
)
|
|
await self.on_transaction_dict(stream)
|
|
await self.generate(1)
|
|
await self.on_transaction_dict(stream)
|
|
|
|
|
|
def get_examples():
|
|
player = Examples('play')
|
|
result = player.run()
|
|
if result.errors:
|
|
for error in result.errors:
|
|
print(error[1])
|
|
raise Exception('See above for errors while running the examples.')
|
|
return player.recorder.examples
|
|
|
|
|
|
SECTIONS = re.compile("(.*?)Usage:(.*?)Options:(.*?)Returns:(.*)", re.DOTALL)
|
|
REQUIRED_OPTIONS = re.compile(r"\(<(.*?)>.*?\)")
|
|
ARGUMENT_NAME = re.compile("--([^=]+)")
|
|
ARGUMENT_TYPE = re.compile(r"\s*\((.*?)\)(.*)")
|
|
|
|
|
|
def get_return_def(returns):
|
|
result = returns.strip()
|
|
if (result[0], result[-1]) == ('{', '}'):
|
|
obj_type = result[1:-1]
|
|
if '[' in obj_type:
|
|
sub_type = obj_type[obj_type.index('[')+1:-1]
|
|
obj_type = obj_type[:obj_type.index('[')]
|
|
if obj_type == 'Paginated':
|
|
obj_def = encode_pagination_doc(RETURN_DOCS[sub_type])
|
|
elif obj_type == 'List':
|
|
obj_def = [RETURN_DOCS[sub_type]]
|
|
else:
|
|
raise NameError(f'Unknown return type: {obj_type}')
|
|
else:
|
|
obj_def = RETURN_DOCS[obj_type]
|
|
return indent(json.dumps(obj_def, indent=4), ' '*12)
|
|
return result
|
|
|
|
|
|
def get_api(name, examples):
|
|
obj = Daemon.callable_methods[name]
|
|
docstr = inspect.getdoc(obj).strip()
|
|
|
|
try:
|
|
description, usage, options, returns = SECTIONS.search(docstr).groups()
|
|
except:
|
|
raise ValueError(f"Doc string format error for {obj.__name__}.")
|
|
|
|
required = re.findall(REQUIRED_OPTIONS, usage)
|
|
|
|
arguments = []
|
|
for line in options.splitlines():
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
if line.startswith('--'):
|
|
arg, desc = line.split(':', 1)
|
|
arg_name = ARGUMENT_NAME.search(arg).group(1)
|
|
arg_type, arg_desc = ARGUMENT_TYPE.search(desc).groups()
|
|
arguments.append({
|
|
'name': arg_name.strip(),
|
|
'type': arg_type.strip(),
|
|
'description': [arg_desc.strip()],
|
|
'is_required': arg_name in required
|
|
})
|
|
elif line == 'None':
|
|
continue
|
|
else:
|
|
arguments[-1]['description'].append(line.strip())
|
|
|
|
for arg in arguments:
|
|
arg['description'] = ' '.join(arg['description'])
|
|
|
|
return {
|
|
'name': name,
|
|
'description': description.strip(),
|
|
'arguments': arguments,
|
|
'returns': get_return_def(returns),
|
|
'examples': examples
|
|
}
|
|
|
|
|
|
def write_api(f):
|
|
examples = get_examples()
|
|
api_definitions = Daemon.get_api_definitions()
|
|
apis = {
|
|
'main': {
|
|
'doc': 'Ungrouped commands.',
|
|
'commands': []
|
|
}
|
|
}
|
|
for group_name, group_doc in api_definitions['groups'].items():
|
|
apis[group_name] = {
|
|
'doc': group_doc,
|
|
'commands': []
|
|
}
|
|
for method_name, command in api_definitions['commands'].items():
|
|
if 'replaced_by' in command:
|
|
continue
|
|
apis[command['group'] or 'main']['commands'].append(get_api(
|
|
method_name,
|
|
examples.get(method_name, [])
|
|
))
|
|
json.dump(apis, f, indent=4)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
html_file = os.path.join(parent, 'docs', 'api.json')
|
|
with open(html_file, 'w+') as f:
|
|
write_api(f)
|