Unified CLI, python 3(WIP) (#1330)

* Added new custom cli class using aiohttp
* Proper error handling in CLI based on RPC error codes(PoC)
* Auth API working
* UnitTests
This commit is contained in:
hackrush 2018-08-05 02:49:10 +05:30 committed by Jack Robison
parent f41229cb5b
commit a7ef8889dd
No known key found for this signature in database
GPG key ID: DF25C68FE0239BB2
14 changed files with 280 additions and 158 deletions

View file

@ -1,36 +1,52 @@
import sys import sys
import json import json
import asyncio import asyncio
import aiohttp from aiohttp.client_exceptions import ClientConnectorError
from requests.exceptions import ConnectionError
from docopt import docopt from docopt import docopt
from textwrap import dedent from textwrap import dedent
from lbrynet.daemon.auth.client import LBRYAPIClient
from lbrynet.core.system_info import get_platform from lbrynet.core.system_info import get_platform
from lbrynet.daemon.Daemon import Daemon from lbrynet.daemon.Daemon import Daemon
from lbrynet.daemon.DaemonControl import start from lbrynet.daemon.DaemonControl import start as daemon_main
from lbrynet.daemon.DaemonConsole import main as daemon_console
async def execute_command(command, args): async def execute_command(method, params, conf_path=None):
message = {'method': command, 'params': args} # this check if the daemon is running or not
async with aiohttp.ClientSession() as session: try:
async with session.get('http://localhost:5279/lbryapi', json=message) as resp: api = LBRYAPIClient.get_client(conf_path)
print(json.dumps(await resp.json(), indent=4)) await api.status()
except (ClientConnectorError, ConnectionError):
print("Could not connect to daemon. Are you sure it's running?")
return 1
# this actually executes the method
try:
resp = await api.call(method, params)
print(json.dumps(resp["result"], indent=2))
except KeyError:
if resp["error"]["code"] == -32500:
print(json.dumps(resp["error"], indent=2))
else:
print(json.dumps(resp["error"]["message"], indent=2))
def print_help(): def print_help():
print(dedent(""" print(dedent("""
NAME NAME
lbry - LBRY command line client. lbrynet - LBRY command line client.
USAGE USAGE
lbry [--conf <config file>] <command> [<args>] lbrynet [--conf <config file>] <command> [<args>]
EXAMPLES EXAMPLES
lbry commands # list available commands lbrynet commands # list available commands
lbry status # get daemon status lbrynet status # get daemon status
lbry --conf ~/l1.conf status # like above but using ~/l1.conf as config file lbrynet --conf ~/l1.conf status # like above but using ~/l1.conf as config file
lbry resolve_name what # resolve a name lbrynet resolve_name what # resolve a name
lbry help resolve_name # get help for a command lbrynet help resolve_name # get help for a command
""")) """))
@ -42,14 +58,14 @@ def print_help_for_command(command):
print("Invalid command name") print("Invalid command name")
def guess_type(x, key=None): def normalize_value(x, key=None):
if not isinstance(x, str): if not isinstance(x, str):
return x return x
if key in ('uri', 'channel_name', 'name', 'file_name', 'download_directory'): if key in ('uri', 'channel_name', 'name', 'file_name', 'download_directory'):
return x return x
if x in ('true', 'True', 'TRUE'): if x.lower() == 'true':
return True return True
if x in ('false', 'False', 'FALSE'): if x.lower() == 'false':
return False return False
if '.' in x: if '.' in x:
try: try:
@ -79,7 +95,7 @@ def set_kwargs(parsed_args):
k = remove_brackets(key[2:]) k = remove_brackets(key[2:])
elif remove_brackets(key) not in kwargs: elif remove_brackets(key) not in kwargs:
k = remove_brackets(key) k = remove_brackets(key)
kwargs[k] = guess_type(arg, k) kwargs[k] = normalize_value(arg, k)
return kwargs return kwargs
@ -89,6 +105,16 @@ def main(argv=None):
print_help() print_help()
return 1 return 1
conf_path = None
if len(argv) and argv[0] == "--conf":
if len(argv) < 2:
print("No config file specified for --conf option")
print_help()
return 1
conf_path = argv[1]
argv = argv[2:]
method, args = argv[0], argv[1:] method, args = argv[0], argv[1:]
if method in ['help', '--help', '-h']: if method in ['help', '--help', '-h']:
@ -96,24 +122,31 @@ def main(argv=None):
print_help_for_command(args[0]) print_help_for_command(args[0])
else: else:
print_help() print_help()
return 0
elif method in ['version', '--version', '-v']: elif method in ['version', '--version', '-v']:
print(json.dumps(get_platform(get_ip=False), sort_keys=True, indent=4, separators=(',', ': '))) print(json.dumps(get_platform(get_ip=False), sort_keys=True, indent=2, separators=(',', ': ')))
return 0
elif method == 'start': elif method == 'start':
start(args) sys.exit(daemon_main(args, conf_path))
elif method == 'console':
sys.exit(daemon_console())
elif method not in Daemon.callable_methods: elif method not in Daemon.callable_methods:
print('"{}" is not a valid command.'.format(method)) if method not in Daemon.deprecated_methods:
print('{} is not a valid command.'.format(method))
return 1 return 1
new_method = Daemon.deprecated_methods[method].new_command
print("{} is deprecated, using {}.".format(method, new_method))
method = new_method
else:
fn = Daemon.callable_methods[method] fn = Daemon.callable_methods[method]
parsed = docopt(fn.__doc__, args) parsed = docopt(fn.__doc__, args)
kwargs = set_kwargs(parsed) params = set_kwargs(parsed)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(execute_command(method, kwargs)) loop.run_until_complete(execute_command(method, params, conf_path))
return 0 return 0

View file

@ -1,3 +1,7 @@
class RPCError(Exception):
code = 0
class PriceDisagreementError(Exception): class PriceDisagreementError(Exception):
pass pass
@ -41,8 +45,8 @@ class NullFundsError(Exception):
pass pass
class InsufficientFundsError(Exception): class InsufficientFundsError(RPCError):
pass code = -310
class ConnectionClosedBeforeResponseError(Exception): class ConnectionClosedBeforeResponseError(Exception):
@ -77,11 +81,13 @@ class UnknownURI(Exception):
super().__init__('URI {} cannot be resolved'.format(uri)) super().__init__('URI {} cannot be resolved'.format(uri))
self.name = uri self.name = uri
class UnknownOutpoint(Exception): class UnknownOutpoint(Exception):
def __init__(self, outpoint): def __init__(self, outpoint):
super().__init__('Outpoint {} cannot be resolved'.format(outpoint)) super().__init__('Outpoint {} cannot be resolved'.format(outpoint))
self.outpoint = outpoint self.outpoint = outpoint
class InvalidName(Exception): class InvalidName(Exception):
def __init__(self, name, invalid_characters): def __init__(self, name, invalid_characters):
self.name = name self.name = name

View file

@ -101,7 +101,7 @@ def obfuscate(plain):
return rot13(base64.b64encode(plain).decode()) return rot13(base64.b64encode(plain).decode())
def check_connection(server="lbry.io", port=80, timeout=2): def check_connection(server="lbry.io", port=80, timeout=5):
"""Attempts to open a socket to server:port and returns True if successful.""" """Attempts to open a socket to server:port and returns True if successful."""
log.debug('Checking connection to %s:%s', server, port) log.debug('Checking connection to %s:%s', server, port)
try: try:

View file

@ -71,7 +71,7 @@ def main():
if method not in Daemon.deprecated_methods: if method not in Daemon.deprecated_methods:
print_error("\"%s\" is not a valid command." % method) print_error("\"%s\" is not a valid command." % method)
return return
new_method = Daemon.deprecated_methods[method]._new_command new_method = Daemon.deprecated_methods[method].new_command
print_error("\"%s\" is deprecated, using \"%s\"." % (method, new_method)) print_error("\"%s\" is deprecated, using \"%s\"." % (method, new_method))
method = new_method method = new_method

View file

@ -1,8 +1,11 @@
import sys import sys
import code import code
import argparse import argparse
import asyncio
import logging.handlers import logging.handlers
from twisted.internet import defer, reactor, threads from twisted.internet import defer, reactor, threads
from aiohttp import client_exceptions
from lbrynet import analytics from lbrynet import analytics
from lbrynet import conf from lbrynet import conf
from lbrynet.core import utils from lbrynet.core import utils
@ -10,8 +13,6 @@ from lbrynet.core import log_support
from lbrynet.daemon.auth.client import LBRYAPIClient from lbrynet.daemon.auth.client import LBRYAPIClient
from lbrynet.daemon.Daemon import Daemon from lbrynet.daemon.Daemon import Daemon
get_client = LBRYAPIClient.get_client
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -114,7 +115,7 @@ def get_methods(daemon):
locs = {} locs = {}
def wrapped(name, fn): def wrapped(name, fn):
client = get_client() client = LBRYAPIClient.get_client()
_fn = getattr(client, name) _fn = getattr(client, name)
_fn.__doc__ = fn.__doc__ _fn.__doc__ = fn.__doc__
return {name: _fn} return {name: _fn}
@ -181,18 +182,18 @@ def threaded_terminal(started_daemon, quiet):
d.addErrback(log.exception) d.addErrback(log.exception)
def start_lbrynet_console(quiet, use_existing_daemon, useauth): async def start_lbrynet_console(quiet, use_existing_daemon, useauth):
if not utils.check_connection(): if not utils.check_connection():
print("Not connected to internet, unable to start") print("Not connected to internet, unable to start")
raise Exception("Not connected to internet, unable to start") raise Exception("Not connected to internet, unable to start")
if not quiet: if not quiet:
print("Starting lbrynet-console...") print("Starting lbrynet-console...")
try: try:
get_client().status() await LBRYAPIClient.get_client().status()
d = defer.succeed(False) d = defer.succeed(False)
if not quiet: if not quiet:
print("lbrynet-daemon is already running, connecting to it...") print("lbrynet-daemon is already running, connecting to it...")
except: except client_exceptions.ClientConnectorError:
if not use_existing_daemon: if not use_existing_daemon:
if not quiet: if not quiet:
print("Starting lbrynet-daemon...") print("Starting lbrynet-daemon...")
@ -222,7 +223,8 @@ def main():
"--http-auth", dest="useauth", action="store_true", default=conf.settings['use_auth_http'] "--http-auth", dest="useauth", action="store_true", default=conf.settings['use_auth_http']
) )
args = parser.parse_args() args = parser.parse_args()
start_lbrynet_console(args.quiet, args.use_existing_daemon, args.useauth) loop = asyncio.get_event_loop()
loop.run_until_complete(start_lbrynet_console(args.quiet, args.use_existing_daemon, args.useauth))
reactor.run() reactor.run()

View file

@ -13,7 +13,6 @@ import argparse
import logging.handlers import logging.handlers
from twisted.internet import reactor from twisted.internet import reactor
#from jsonrpc.proxy import JSONRPCProxy
from lbrynet import conf from lbrynet import conf
from lbrynet.core import utils, system_info from lbrynet.core import utils, system_info
@ -26,20 +25,13 @@ def test_internet_connection():
return utils.check_connection() return utils.check_connection()
def start(argv): def start(argv=None, conf_path=None):
"""The primary entry point for launching the daemon.""" if conf_path is not None:
conf.conf_file = conf_path
# postpone loading the config file to after the CLI arguments conf.initialize_settings()
# have been parsed, as they may contain an alternate config file location
conf.initialize_settings(load_conf_file=False)
parser = argparse.ArgumentParser(description="Launch lbrynet-daemon") parser = argparse.ArgumentParser()
parser.add_argument(
"--conf",
help="specify an alternative configuration file",
type=str,
default=None
)
parser.add_argument( parser.add_argument(
"--http-auth", dest="useauth", action="store_true", default=conf.settings['use_auth_http'] "--http-auth", dest="useauth", action="store_true", default=conf.settings['use_auth_http']
) )
@ -58,9 +50,8 @@ def start(argv):
) )
args = parser.parse_args(argv) args = parser.parse_args(argv)
update_settings_from_args(args) if args.useauth:
conf.settings.update({'use_auth_http': args.useauth}, data_types=(conf.TYPE_CLI,))
conf.settings.load_conf_file_settings()
if args.version: if args.version:
version = system_info.get_platform(get_ip=False) version = system_info.get_platform(get_ip=False)
@ -90,17 +81,3 @@ def start(argv):
reactor.run() reactor.run()
else: else:
log.info("Not connected to internet, unable to start") log.info("Not connected to internet, unable to start")
def update_settings_from_args(args):
if args.conf:
conf.conf_file = args.conf
if args.useauth:
conf.settings.update({
'use_auth_http': args.useauth,
}, data_types=(conf.TYPE_CLI,))
if __name__ == "__main__":
start(sys.argv[1:])

View file

@ -39,8 +39,12 @@ class PasswordChecker:
return cls(passwords) return cls(passwords)
def requestAvatarId(self, creds): def requestAvatarId(self, creds):
if creds.username in self.passwords: password_dict_bytes = {}
pw = self.passwords.get(creds.username) for api in self.passwords:
password_dict_bytes.update({api.encode(): self.passwords[api].encode()})
if creds.username in password_dict_bytes:
pw = password_dict_bytes.get(creds.username)
pw_match = creds.checkPassword(pw) pw_match = creds.checkPassword(pw)
if pw_match: if pw_match:
return defer.succeed(creds.username) return defer.succeed(creds.username)

View file

@ -1,11 +1,11 @@
# pylint: skip-file # pylint: skip-file
import os import os
import json import json
import urlparse import aiohttp
from urllib.parse import urlparse
import requests import requests
from requests.cookies import RequestsCookieJar from requests.cookies import RequestsCookieJar
import logging import logging
from jsonrpc.proxy import JSONRPCProxy
from lbrynet import conf from lbrynet import conf
from lbrynet.daemon.auth.util import load_api_keys, APIKey, API_KEY_NAME, get_auth_message from lbrynet.daemon.auth.util import load_api_keys, APIKey, API_KEY_NAME, get_auth_message
@ -14,6 +14,7 @@ USER_AGENT = "AuthServiceProxy/0.1"
TWISTED_SESSION = "TWISTED_SESSION" TWISTED_SESSION = "TWISTED_SESSION"
LBRY_SECRET = "LBRY_SECRET" LBRY_SECRET = "LBRY_SECRET"
HTTP_TIMEOUT = 30 HTTP_TIMEOUT = 30
SCHEME = "http"
def copy_cookies(cookies): def copy_cookies(cookies):
@ -28,6 +29,32 @@ class JSONRPCException(Exception):
self.error = rpc_error self.error = rpc_error
class UnAuthAPIClient:
def __init__(self, host, port):
self.host = host
self.port = port
self.scheme = SCHEME
def __getattr__(self, method):
async def f(*args, **kwargs):
return await self.call(method, [args, kwargs])
return f
@classmethod
def from_url(cls, url):
url_fragment = urlparse(url)
host = url_fragment.hostname
port = url_fragment.port
return cls(host, port)
async def call(self, method, params=None):
message = {'method': method, 'params': params}
async with aiohttp.ClientSession() as session:
async with session.get('{}://{}:{}'.format(self.scheme, self.host, self.port), json=message) as resp:
return await resp.json()
class AuthAPIClient: class AuthAPIClient:
def __init__(self, key, timeout, connection, count, cookies, url, login_url): def __init__(self, key, timeout, connection, count, cookies, url, login_url):
self.__api_key = key self.__api_key = key
@ -46,7 +73,7 @@ class AuthAPIClient:
return f return f
def call(self, method, params=None): async def call(self, method, params=None):
params = params or {} params = params or {}
self.__id_count += 1 self.__id_count += 1
pre_auth_post_data = { pre_auth_post_data = {
@ -56,9 +83,10 @@ class AuthAPIClient:
'id': self.__id_count 'id': self.__id_count
} }
to_auth = get_auth_message(pre_auth_post_data) to_auth = get_auth_message(pre_auth_post_data)
pre_auth_post_data.update({'hmac': self.__api_key.get_hmac(to_auth)}) pre_auth_post_data.update({'hmac': self.__api_key.get_hmac(to_auth).decode()})
post_data = json.dumps(pre_auth_post_data) post_data = json.dumps(pre_auth_post_data)
cookies = copy_cookies(self.__cookies) cookies = copy_cookies(self.__cookies)
req = requests.Request( req = requests.Request(
method='POST', url=self.__service_url, data=post_data, cookies=cookies, method='POST', url=self.__service_url, data=post_data, cookies=cookies,
headers={ headers={
@ -69,21 +97,13 @@ class AuthAPIClient:
) )
http_response = self.__conn.send(req.prepare()) http_response = self.__conn.send(req.prepare())
if http_response is None: if http_response is None:
raise JSONRPCException({ raise JSONRPCException({'code': -342, 'message': 'missing HTTP response from server'})
'code': -342, 'message': 'missing HTTP response from server'})
http_response.raise_for_status() http_response.raise_for_status()
next_secret = http_response.headers.get(LBRY_SECRET, False) next_secret = http_response.headers.get(LBRY_SECRET, False)
if next_secret: if next_secret:
self.__api_key.secret = next_secret self.__api_key.secret = next_secret
self.__cookies = copy_cookies(http_response.cookies) self.__cookies = copy_cookies(http_response.cookies)
response = http_response.json() return http_response.json()
if response.get('error') is not None:
raise JSONRPCException(response['error'])
elif 'result' not in response:
raise JSONRPCException({
'code': -343, 'message': 'missing JSON-RPC result'})
else:
return response['result']
@classmethod @classmethod
def config(cls, key_name=None, key=None, pw_path=None, timeout=HTTP_TIMEOUT, connection=None, count=0, def config(cls, key_name=None, key=None, pw_path=None, timeout=HTTP_TIMEOUT, connection=None, count=0,
@ -97,24 +117,23 @@ class AuthAPIClient:
else: else:
api_key = APIKey(name=api_key_name, secret=key) api_key = APIKey(name=api_key_name, secret=key)
if login_url is None: if login_url is None:
service_url = "http://%s:%s@%s:%i/%s" % (api_key_name, service_url = "http://{}:{}@{}:{}".format(
api_key.secret, api_key_name, api_key.secret, conf.settings['api_host'], conf.settings['api_port']
conf.settings['api_host'], )
conf.settings['api_port'],
conf.settings['API_ADDRESS'])
else: else:
service_url = login_url service_url = login_url
id_count = count id_count = count
if auth is None and connection is None and cookies is None and url is None: if auth is None and connection is None and cookies is None and url is None:
# This is a new client instance, start an authenticated session # This is a new client instance, start an authenticated session
url = urlparse.urlparse(service_url) url = urlparse(service_url)
conn = requests.Session() conn = requests.Session()
req = requests.Request(method='POST', req = requests.Request(
url=service_url, method='POST', url=service_url, headers={
headers={'Host': url.hostname, 'Host': url.hostname,
'User-Agent': USER_AGENT, 'User-Agent': USER_AGENT,
'Content-type': 'application/json'},) 'Content-type': 'application/json'
})
r = req.prepare() r = req.prepare()
http_response = conn.send(r) http_response = conn.send(r)
cookies = RequestsCookieJar() cookies = RequestsCookieJar()
@ -133,8 +152,9 @@ class AuthAPIClient:
class LBRYAPIClient: class LBRYAPIClient:
@staticmethod @staticmethod
def get_client(): def get_client(conf_path=None):
conf.conf_file = conf_path
if not conf.settings: if not conf.settings:
conf.initialize_settings() conf.initialize_settings()
return AuthAPIClient.config() if conf.settings['use_auth_http'] else \ return AuthAPIClient.config() if conf.settings['use_auth_http'] else \
JSONRPCProxy.from_url(conf.settings.get_api_connection_string()) UnAuthAPIClient.from_url(conf.settings.get_api_connection_string())

View file

@ -81,8 +81,8 @@ class JSONRPCError:
} }
@classmethod @classmethod
def create_from_exception(cls, exception, code=CODE_APPLICATION_ERROR, traceback=None): def create_from_exception(cls, message, code=CODE_APPLICATION_ERROR, traceback=None):
return cls(exception.message, code=code, traceback=traceback) return cls(message, code=code, traceback=traceback)
def default_decimal(obj): def default_decimal(obj):
@ -109,8 +109,7 @@ def jsonrpc_dumps_pretty(obj, **kwargs):
else: else:
data = {"jsonrpc": "2.0", "result": obj, "id": id_} data = {"jsonrpc": "2.0", "result": obj, "id": id_}
return json.dumps(data, cls=jsonrpclib.JSONRPCEncoder, sort_keys=True, indent=2, return json.dumps(data, cls=jsonrpclib.JSONRPCEncoder, sort_keys=True, indent=2, **kwargs) + "\n"
separators=(',', ': '), **kwargs) + "\n"
class JSONRPCServerType(type): class JSONRPCServerType(type):
@ -134,7 +133,7 @@ class AuthorizedBase(metaclass=JSONRPCServerType):
@staticmethod @staticmethod
def deprecated(new_command=None): def deprecated(new_command=None):
def _deprecated_wrapper(f): def _deprecated_wrapper(f):
f._new_command = new_command f.new_command = new_command
f._deprecated = True f._deprecated = True
return f return f
return _deprecated_wrapper return _deprecated_wrapper
@ -284,8 +283,8 @@ class AuthJSONRPCServer(AuthorizedBase):
request.setHeader(LBRY_SECRET, self.sessions.get(session_id).secret) request.setHeader(LBRY_SECRET, self.sessions.get(session_id).secret)
@staticmethod @staticmethod
def _render_message(request, message): def _render_message(request, message: str):
request.write(message) request.write(message.encode())
request.finish() request.finish()
def _render_error(self, failure, request, id_): def _render_error(self, failure, request, id_):
@ -296,8 +295,15 @@ class AuthJSONRPCServer(AuthorizedBase):
error = failure.check(JSONRPCError) error = failure.check(JSONRPCError)
if error is None: if error is None:
# maybe its a twisted Failure with another type of error # maybe its a twisted Failure with another type of error
error = JSONRPCError(failure.getErrorMessage() or failure.type.__name__, if hasattr(failure.type, "code"):
traceback=failure.getTraceback()) error_code = failure.type.code
else:
error_code = JSONRPCError.CODE_APPLICATION_ERROR
error = JSONRPCError.create_from_exception(
failure.getErrorMessage() or failure.type.__name__,
code=error_code,
traceback=failure.getTraceback()
)
if not failure.check(ComponentsNotStarted, ComponentStartConditionNotMet): if not failure.check(ComponentsNotStarted, ComponentStartConditionNotMet):
log.warning("error processing api request: %s\ntraceback: %s", error.message, log.warning("error processing api request: %s\ntraceback: %s", error.message,
"\n".join(error.traceback)) "\n".join(error.traceback))
@ -321,7 +327,7 @@ class AuthJSONRPCServer(AuthorizedBase):
return self._render(request) return self._render(request)
except BaseException as e: except BaseException as e:
log.error(e) log.error(e)
error = JSONRPCError.create_from_exception(e, traceback=format_exc()) error = JSONRPCError.create_from_exception(str(e), traceback=format_exc())
self._render_error(error, request, None) self._render_error(error, request, None)
return server.NOT_DONE_YET return server.NOT_DONE_YET
@ -352,12 +358,12 @@ class AuthJSONRPCServer(AuthorizedBase):
session.touch() session.touch()
request.content.seek(0, 0) request.content.seek(0, 0)
content = request.content.read() content = request.content.read().decode()
try: try:
parsed = jsonrpclib.loads(content) parsed = jsonrpclib.loads(content)
except ValueError: except json.JSONDecodeError:
log.warning("Unable to decode request json") log.warning("Unable to decode request json")
self._render_error(JSONRPCError(None, JSONRPCError.CODE_PARSE_ERROR), request, None) self._render_error(JSONRPCError(None, code=JSONRPCError.CODE_PARSE_ERROR), request, None)
return server.NOT_DONE_YET return server.NOT_DONE_YET
request_id = None request_id = None
@ -381,7 +387,8 @@ class AuthJSONRPCServer(AuthorizedBase):
log.warning("API validation failed") log.warning("API validation failed")
self._render_error( self._render_error(
JSONRPCError.create_from_exception( JSONRPCError.create_from_exception(
err, code=JSONRPCError.CODE_AUTHENTICATION_ERROR, str(err),
code=JSONRPCError.CODE_AUTHENTICATION_ERROR,
traceback=format_exc() traceback=format_exc()
), ),
request, request_id request, request_id
@ -396,7 +403,7 @@ class AuthJSONRPCServer(AuthorizedBase):
except UnknownAPIMethodError as err: except UnknownAPIMethodError as err:
log.warning('Failed to get function %s: %s', function_name, err) log.warning('Failed to get function %s: %s', function_name, err)
self._render_error( self._render_error(
JSONRPCError(None, JSONRPCError.CODE_METHOD_NOT_FOUND), JSONRPCError(None, code=JSONRPCError.CODE_METHOD_NOT_FOUND),
request, request_id request, request_id
) )
return server.NOT_DONE_YET return server.NOT_DONE_YET
@ -507,7 +514,7 @@ class AuthJSONRPCServer(AuthorizedBase):
def _get_jsonrpc_method(self, function_path): def _get_jsonrpc_method(self, function_path):
if function_path in self.deprecated_methods: if function_path in self.deprecated_methods:
new_command = self.deprecated_methods[function_path]._new_command new_command = self.deprecated_methods[function_path].new_command
log.warning('API function \"%s\" is deprecated, please update to use \"%s\"', log.warning('API function \"%s\" is deprecated, please update to use \"%s\"',
function_path, new_command) function_path, new_command)
function_path = new_command function_path = new_command
@ -565,10 +572,10 @@ class AuthJSONRPCServer(AuthorizedBase):
def _callback_render(self, result, request, id_, auth_required=False): def _callback_render(self, result, request, id_, auth_required=False):
try: try:
encoded_message = jsonrpc_dumps_pretty(result, id=id_, default=default_decimal).encode() message = jsonrpc_dumps_pretty(result, id=id_, default=default_decimal)
request.setResponseCode(200) request.setResponseCode(200)
self._set_headers(request, encoded_message, auth_required) self._set_headers(request, message, auth_required)
self._render_message(request, encoded_message) self._render_message(request, message)
except Exception as err: except Exception as err:
log.exception("Failed to render API response: %s", result) log.exception("Failed to render API response: %s", result)
self._render_error(err, request, id_) self._render_error(err, request, id_)

View file

@ -12,12 +12,12 @@ API_KEY_NAME = "api"
LBRY_SECRET = "LBRY_SECRET" LBRY_SECRET = "LBRY_SECRET"
def sha(x): def sha(x: bytes) -> bytes:
h = hashlib.sha256(x).digest() h = hashlib.sha256(x).digest()
return base58.b58encode(h) return base58.b58encode(h)
def generate_key(x=None): def generate_key(x: bytes=None) -> bytes:
if x is None: if x is None:
return sha(os.urandom(256)) return sha(os.urandom(256))
else: else:
@ -41,7 +41,7 @@ class APIKey:
def get_hmac(self, message): def get_hmac(self, message):
decoded_key = self._raw_key() decoded_key = self._raw_key()
signature = hmac.new(decoded_key, message, hashlib.sha256) signature = hmac.new(decoded_key, message.encode(), hashlib.sha256)
return base58.b58encode(signature.digest()) return base58.b58encode(signature.digest())
def compare_hmac(self, message, token): def compare_hmac(self, message, token):
@ -66,7 +66,7 @@ def load_api_keys(path):
keys_for_return = {} keys_for_return = {}
for key_name in data: for key_name in data:
key = data[key_name] key = data[key_name]
secret = key['secret'] secret = key['secret'].decode()
expiration = key['expiration'] expiration = key['expiration']
keys_for_return.update({key_name: APIKey(secret, key_name, expiration)}) keys_for_return.update({key_name: APIKey(secret, key_name, expiration)})
return keys_for_return return keys_for_return

View file

View file

@ -0,0 +1,6 @@
from lbrynet import conf
from lbrynet import cli
class CLIIntegrationTest:
pass

100
tests/test_cli.py Normal file
View file

@ -0,0 +1,100 @@
import contextlib
import json
from io import StringIO
from twisted.trial import unittest
from lbrynet.core.system_info import get_platform
from lbrynet.cli import normalize_value, main
class CLITest(unittest.TestCase):
def test_guess_type(self):
self.assertEqual('0.3.8', normalize_value('0.3.8'))
self.assertEqual(0.3, normalize_value('0.3'))
self.assertEqual(3, normalize_value('3'))
self.assertEqual(3, normalize_value(3))
self.assertEqual(
'VdNmakxFORPSyfCprAD/eDDPk5TY9QYtSA==',
normalize_value('VdNmakxFORPSyfCprAD/eDDPk5TY9QYtSA==')
)
self.assertEqual(True, normalize_value('TRUE'))
self.assertEqual(True, normalize_value('true'))
self.assertEqual(True, normalize_value('TrUe'))
self.assertEqual(False, normalize_value('FALSE'))
self.assertEqual(False, normalize_value('false'))
self.assertEqual(False, normalize_value('FaLsE'))
self.assertEqual(True, normalize_value(True))
self.assertEqual('3', normalize_value('3', key="uri"))
self.assertEqual('0.3', normalize_value('0.3', key="uri"))
self.assertEqual('True', normalize_value('True', key="uri"))
self.assertEqual('False', normalize_value('False', key="uri"))
self.assertEqual('3', normalize_value('3', key="file_name"))
self.assertEqual('3', normalize_value('3', key="name"))
self.assertEqual('3', normalize_value('3', key="download_directory"))
self.assertEqual('3', normalize_value('3', key="channel_name"))
self.assertEqual(3, normalize_value('3', key="some_other_thing"))
def test_help_command(self):
actual_output = StringIO()
with contextlib.redirect_stdout(actual_output):
main(['help'])
actual_output = actual_output.getvalue()
self.assertSubstring('lbrynet - LBRY command line client.', actual_output)
self.assertSubstring('USAGE', actual_output)
def test_help_for_command_command(self):
actual_output = StringIO()
with contextlib.redirect_stdout(actual_output):
main(['help', 'publish'])
actual_output = actual_output.getvalue()
self.assertSubstring('Make a new name claim and publish', actual_output)
self.assertSubstring('Usage:', actual_output)
def test_help_for_command_command_with_invalid_command(self):
actual_output = StringIO()
with contextlib.redirect_stdout(actual_output):
main(['help', 'publish1'])
self.assertSubstring('Invalid command name', actual_output.getvalue())
def test_version_command(self):
actual_output = StringIO()
with contextlib.redirect_stdout(actual_output):
main(['version'])
self.assertEqual(
actual_output.getvalue().strip(),
json.dumps(get_platform(get_ip=False), sort_keys=True, indent=2)
)
def test_invalid_command(self):
actual_output = StringIO()
with contextlib.redirect_stdout(actual_output):
main(['publish1'])
self.assertEqual(
actual_output.getvalue().strip(),
"publish1 is not a valid command."
)
def test_valid_command_daemon_not_started(self):
actual_output = StringIO()
with contextlib.redirect_stdout(actual_output):
main(["publish", '--name=asd', '--bid=99'])
self.assertEqual(
actual_output.getvalue().strip(),
"Could not connect to daemon. Are you sure it's running?"
)
def test_deprecated_command_daemon_not_started(self):
actual_output = StringIO()
with contextlib.redirect_stdout(actual_output):
main(["channel_list_mine"])
self.assertEqual(
actual_output.getvalue().strip(),
"channel_list_mine is deprecated, using channel_list.\n"
"Could not connect to daemon. Are you sure it's running?"
)

View file

@ -1,33 +0,0 @@
from unittest import skip
from twisted.trial import unittest
# from lbrynet.daemon import DaemonCLI
@skip('cli is being rewritten to work in py3')
class DaemonCLITests(unittest.TestCase):
def test_guess_type(self):
self.assertEqual('0.3.8', DaemonCLI.guess_type('0.3.8'))
self.assertEqual(0.3, DaemonCLI.guess_type('0.3'))
self.assertEqual(3, DaemonCLI.guess_type('3'))
self.assertEqual('VdNmakxFORPSyfCprAD/eDDPk5TY9QYtSA==',
DaemonCLI.guess_type('VdNmakxFORPSyfCprAD/eDDPk5TY9QYtSA=='))
self.assertEqual(0.3, DaemonCLI.guess_type('0.3'))
self.assertEqual(True, DaemonCLI.guess_type('TRUE'))
self.assertEqual(True, DaemonCLI.guess_type('true'))
self.assertEqual(True, DaemonCLI.guess_type('True'))
self.assertEqual(False, DaemonCLI.guess_type('FALSE'))
self.assertEqual(False, DaemonCLI.guess_type('false'))
self.assertEqual(False, DaemonCLI.guess_type('False'))
self.assertEqual('3', DaemonCLI.guess_type('3', key="uri"))
self.assertEqual('0.3', DaemonCLI.guess_type('0.3', key="uri"))
self.assertEqual('True', DaemonCLI.guess_type('True', key="uri"))
self.assertEqual('False', DaemonCLI.guess_type('False', key="uri"))
self.assertEqual('3', DaemonCLI.guess_type('3', key="file_name"))
self.assertEqual('3', DaemonCLI.guess_type('3', key="name"))
self.assertEqual('3', DaemonCLI.guess_type('3', key="download_directory"))
self.assertEqual('3', DaemonCLI.guess_type('3', key="channel_name"))
self.assertEqual(3, DaemonCLI.guess_type('3', key="some_other_thing"))