API is now typed and includes sharable argument lists

This commit is contained in:
Lex Berezhny 2020-05-18 08:24:15 -04:00
parent 6986211c1e
commit 5b5c45ea76
6 changed files with 1289 additions and 2088 deletions

View file

@ -0,0 +1,4 @@
from .api import API
from .daemon import Daemon
from .full_node import FullNode
from .light_client import LightClient

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,17 @@
import os import os
import asyncio import asyncio
import logging import logging
import signal
from typing import List, Optional, Tuple, NamedTuple from typing import List, Optional, Tuple, NamedTuple
from aiohttp.web import GracefulExit
from lbry.db import Database from lbry.db import Database
from lbry.db.constants import TXO_TYPES from lbry.db.constants import TXO_TYPES
from lbry.schema.result import Censor from lbry.schema.result import Censor
from lbry.blockchain.transaction import Transaction, Output from lbry.blockchain.transaction import Transaction, Output
from lbry.blockchain.ledger import Ledger from lbry.blockchain.ledger import Ledger
from lbry.wallet import WalletManager, AddressManager from lbry.wallet import WalletManager
from lbry.event import EventController from lbry.event import EventController
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -71,6 +74,30 @@ class Service:
self._on_connected_controller = EventController() self._on_connected_controller = EventController()
self.on_connected = self._on_connected_controller.stream self.on_connected = self._on_connected_controller.stream
def run(self):
loop = asyncio.get_event_loop()
def exit():
raise GracefulExit()
try:
loop.add_signal_handler(signal.SIGINT, exit)
loop.add_signal_handler(signal.SIGTERM, exit)
except NotImplementedError:
pass # Not implemented on Windows
try:
loop.run_until_complete(self.start())
loop.run_forever()
except (GracefulExit, KeyboardInterrupt, asyncio.CancelledError):
pass
finally:
loop.run_until_complete(self.stop())
logging.shutdown()
if hasattr(loop, 'shutdown_asyncgens'):
loop.run_until_complete(loop.shutdown_asyncgens())
async def start(self): async def start(self):
await self.db.open() await self.db.open()
await self.wallets.ensure_path_exists() await self.wallets.ensure_path_exists()
@ -119,11 +146,11 @@ class Service:
self.constraint_spending_utxos(constraints) self.constraint_spending_utxos(constraints)
return self.db.get_utxos(**constraints) return self.db.get_utxos(**constraints)
async def get_txos(self, resolve=False, **constraints) -> List[Output]: async def get_txos(self, resolve=False, **constraints) -> Tuple[List[Output], Optional[int]]:
txos = await self.db.get_txos(**constraints) txos, count = await self.db.get_txos(**constraints)
if resolve: if resolve:
return await self._resolve_for_local_results(constraints.get('accounts', []), txos) return await self._resolve_for_local_results(constraints.get('accounts', []), txos), count
return txos return txos, count
def get_txo_sum(self, **constraints): def get_txo_sum(self, **constraints):
return self.db.get_txo_sum(**constraints) return self.db.get_txo_sum(**constraints)
@ -153,10 +180,10 @@ class Service:
async def search_transactions(self, txids): async def search_transactions(self, txids):
raise NotImplementedError raise NotImplementedError
async def announce_addresses(self, address_manager: AddressManager, addresses: List[str]): async def announce_addresses(self, address_manager, addresses: List[str]):
await self.ledger.announce_addresses(address_manager, addresses) await self.ledger.announce_addresses(address_manager, addresses)
async def get_address_manager_for_address(self, address) -> Optional[AddressManager]: async def get_address_manager_for_address(self, address):
details = await self.db.get_address(address=address) details = await self.db.get_address(address=address)
for account in self.accounts: for account in self.accounts:
if account.id == details['account']: if account.id == details['account']:
@ -177,12 +204,14 @@ class Service:
return self.ledger.genesis_hash return self.ledger.genesis_hash
return (await self.ledger.headers.hash(self.ledger.headers.height)).decode() return (await self.ledger.headers.hash(self.ledger.headers.height)).decode()
async def broadcast_or_release(self, tx, blocking=False): async def maybe_broadcast_or_release(self, tx, blocking=False, preview=False):
if preview:
return await self.release_tx(tx)
try: try:
await self.broadcast(tx) await self.broadcast(tx)
if blocking: if blocking:
await self.wait(tx, timeout=None) await self.wait(tx, timeout=None)
except: except Exception:
await self.release_tx(tx) await self.release_tx(tx)
raise raise

View file

@ -64,6 +64,7 @@ class Daemon:
def __init__(self, service: Service): def __init__(self, service: Service):
self.service = service self.service = service
self.conf = service.conf
self.api = API(service) self.api = API(service)
self.app = Application() self.app = Application()
self.app['websockets'] = WeakSet() self.app['websockets'] = WeakSet()
@ -81,8 +82,7 @@ class Daemon:
async def start(self): async def start(self):
await self.runner.setup() await self.runner.setup()
port = self.service.ledger.conf.api.split(':')[1] site = TCPSite(self.runner, 'localhost', self.conf.api_port)
site = TCPSite(self.runner, 'localhost', port)
await site.start() await site.start()
await self.service.start() await self.service.start()

View file

@ -47,7 +47,7 @@ output_doc = {
transaction_doc = { transaction_doc = {
'txid': "hash of transaction in hex", 'txid': "hash of transaction in hex",
'height': "block where transaction was recorded", 'height': "block where transaction was recorded",
'inputs': [output_doc], 'inputs': ['spent outputs...'],
'outputs': [output_doc], 'outputs': [output_doc],
'total_input': "sum of inputs as a decimal", 'total_input': "sum of inputs as a decimal",
'total_output': "sum of outputs, sans fee, as a decimal", 'total_output': "sum of outputs, sans fee, as a decimal",
@ -109,7 +109,7 @@ managedstream_doc = {
address_doc = { address_doc = {
"address": "(str)"
} }

View file

@ -10,6 +10,9 @@ from lbry.service import api
from lbry.service import json_encoder from lbry.service import json_encoder
LINE_WIDTH = 90
def parse_description(desc) -> dict: def parse_description(desc) -> dict:
lines = iter(desc.splitlines()) lines = iter(desc.splitlines())
parts = {'text': []} parts = {'text': []}
@ -19,7 +22,10 @@ def parse_description(desc) -> dict:
current = parts.setdefault(line.strip().lower()[:-1], []) current = parts.setdefault(line.strip().lower()[:-1], [])
else: else:
if line.strip(): if line.strip():
current.append(line) if line.strip() == '{kwargs}':
parts['kwargs'] = line.find('{kwargs}')
else:
current.append(line)
return parts return parts
@ -36,20 +42,9 @@ def parse_type(tokens: List) -> Tuple[str, str]:
json_ = json_encoder.encode_pagination_doc( json_ = json_encoder.encode_pagination_doc(
getattr(json_encoder, f'{type_[2].lower()}_doc') getattr(json_encoder, f'{type_[2].lower()}_doc')
) )
elif len(type_) == 1 and hasattr(json_encoder, f'{type_[0].lower()}_doc'):
json_ = getattr(json_encoder, f'{type_[0].lower()}_doc')
return ''.join(type_), json_ return ''.join(type_), json_
# 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)
def parse_argument(tokens, method_name='') -> dict: def parse_argument(tokens, method_name='') -> dict:
@ -59,6 +54,10 @@ def parse_argument(tokens, method_name='') -> dict:
} }
if arg['name'] == 'self': if arg['name'] == 'self':
return {} return {}
try:
tokens[0]
except:
a = 9
if tokens[0].string == ':': if tokens[0].string == ':':
tokens.pop(0) tokens.pop(0)
type_tokens = [] type_tokens = []
@ -100,18 +99,20 @@ def produce_argument_tokens(src: str):
if not in_comment and t.string == ',': if not in_comment and t.string == ',':
in_comment = True in_comment = True
elif in_comment and (t.type == token.NAME or t.string == '**'): elif in_comment and (t.type == token.NAME or t.string == '**'):
yield parsed if not parsed[0].string.startswith('_'):
yield parsed
in_comment = False in_comment = False
parsed = [] parsed = []
if t.type in (token.NAME, token.OP, token.COMMENT, token.STRING, token.NUMBER): if t.type in (token.NAME, token.OP, token.COMMENT, token.STRING, token.NUMBER):
parsed.append(t) parsed.append(t)
if t.string == ')': if t.string == ')':
yield parsed if not parsed[0].string.startswith('_'):
yield parsed
break break
def parse_return(tokens) -> dict: def parse_return(tokens) -> dict:
d = {'desc': []} d = {'desc': [], 'type': None}
if tokens[0].string == '->': if tokens[0].string == '->':
tokens.pop(0) tokens.pop(0)
type_tokens = [] type_tokens = []
@ -144,7 +145,7 @@ def produce_return_tokens(src: str):
def parse_method(method, expanders: dict) -> dict: def parse_method(method, expanders: dict) -> dict:
d = { d = {
'name': method.__name__, 'name': method.__name__,
'desc': parse_description(textwrap.dedent(method.__doc__)) if method.__doc__ else '', 'desc': parse_description(textwrap.dedent(method.__doc__)) if method.__doc__ else {},
'method': method, 'method': method,
'arguments': [], 'arguments': [],
'returns': None 'returns': None
@ -153,11 +154,15 @@ def parse_method(method, expanders: dict) -> dict:
for tokens in produce_argument_tokens(src): for tokens in produce_argument_tokens(src):
if tokens[0].string == '**': if tokens[0].string == '**':
tokens.pop(0) tokens.pop(0)
expander_name = tokens.pop(0).string[:-7] d['kwargs'] = []
if expander_name not in expanders: expander_names = tokens.pop(0).string[:-7]
raise Exception(f"Expander '{expander_name}' not found, used by {d['name']}.") if expander_names.startswith('_'):
expander = expanders[expander_name] continue
d['arguments'].extend(expander) for expander_name in expander_names.split('_and_'):
if expander_name not in expanders:
raise Exception(f"Expander '{expander_name}' not found, used by {d['name']}.")
d['arguments'].extend(expanders[expander_name])
d['kwargs'].extend(expanders[expander_name])
else: else:
arg = parse_argument(tokens, d['name']) arg = parse_argument(tokens, d['name'])
if arg: if arg:
@ -168,8 +173,9 @@ def parse_method(method, expanders: dict) -> dict:
def get_expanders(): def get_expanders():
expanders = {} expanders = {}
for e in api.kwarg_expanders: for name, func in api.kwarg_expanders.items():
expanders[e.__name__] = parse_method(e, expanders)['arguments'] if name.endswith('_original'):
expanders[name[:-len('_original')]] = parse_method(func, expanders)['arguments']
return expanders return expanders
@ -188,7 +194,9 @@ def get_methods(cls):
} }
def generate_options(method, indent): def generate_options(method, indent) -> List[str]:
if not method['arguments']:
return []
flags = [] flags = []
for arg in method['arguments']: for arg in method['arguments']:
if arg['type'] == 'bool': if arg['type'] == 'bool':
@ -199,16 +207,69 @@ def generate_options(method, indent):
flags = [f.ljust(max_len) for f in flags] flags = [f.ljust(max_len) for f in flags]
options = [] options = []
for flag, arg in zip(flags, method['arguments']): for flag, arg in zip(flags, method['arguments']):
line = [f"{indent}{flag}: ({arg['type']}) {' '.join(arg['desc'])}"] left = f"{indent}{flag}: "
text = f"({arg['type']}) {' '.join(arg['desc'])}"
if 'default' in arg: if 'default' in arg:
line.append(f" [default: {arg['default']}]") if arg['type'] != 'bool':
options.append(''.join(line)) text += f" [default: {arg['default']}]"
wrapped = textwrap.wrap(text, LINE_WIDTH-len(left))
lines = [f"{left}{wrapped.pop(0)}"]
for line in wrapped:
lines.append(f"{' '*len(left)} {line}")
options.extend(lines)
return options return options
def augment_description(command): def generate_help(command):
pass indent = 4
text = []
desc = command['desc']
for line in desc.get('text', []):
text.append(line)
text.append('')
usage, kwargs_offset = desc.get('usage', []), desc.get('kwargs', False)
text.append('Usage:')
if usage:
for line in usage:
text.append(line)
else:
text.append(f"{' '*indent}{command['cli']}")
if kwargs_offset:
flags = []
for arg in command['kwargs']:
if arg['type'] == 'bool':
flags.append(f"[--{arg['name']}]")
elif 'list' in arg['type']:
flags.append(f"[--{arg['name']}=<{arg['name']}>...]")
else:
flags.append(f"[--{arg['name']}=<{arg['name']}>]")
wrapped = textwrap.wrap(' '.join(flags), LINE_WIDTH-kwargs_offset)
for line in wrapped:
text.append(f"{' '*kwargs_offset}{line}")
text.append('')
options = desc.get('options', [])
if options or command['arguments']:
text.append('Options:')
for line in options:
text.append(line)
text.extend(generate_options(command, ' '*indent))
text.append('')
returns = desc.get('returns', [])
if returns or command['returns']['type']:
text.append('Returns:')
if command['returns']['type']:
return_comment = ' '.join(command['returns']['desc'])
text.append(f"{' '*indent}({command['returns']['type']}) {return_comment}")
text.extend(returns)
if 'json' in command['returns']:
dump = json.dumps(command['returns']['json'], indent=4)
text.extend(textwrap.indent(dump, ' '*indent).splitlines())
return '\n'.join(text)
def get_api_definitions(cls): def get_api_definitions(cls):
@ -219,7 +280,10 @@ def get_api_definitions(cls):
if parts[0] in groups: if parts[0] in groups:
command['name'] = '_'.join(parts[1:]) command['name'] = '_'.join(parts[1:])
command['group'] = parts[0] command['group'] = parts[0]
#command['desc'] = command['cli'] = f"{command['group']} {command['name']}"
else:
command['cli'] = command['name']
command['help'] = generate_help(command)
return {'groups': groups, 'commands': commands} return {'groups': groups, 'commands': commands}