def methodizer(func=None, *, instance_params=()): """A decorator to get method versions of functions. :param func: :param instance_params: :return: >>> from py2http.decorators import methodizer >>> >>> >>> def f(a, b, x): ... return x * (a + b) ... >>> def g(x, y=1): ... return x * y ... >>> methodize = methodizer(instance_params=('x', 'non_existing_attr')) >>> >>> class A: ... def __init__(self, x=0): ... self.x = x ... ... f = methodize(f) ... g = methodize(g) ... >>> >>> a = A(x=3) >>> assert a.f(b=1, a=2) == 9 >>> assert a.g() == 3 >>> assert a.g(y=10) == 30 """ if func is None: return partial(methodizer, instance_params=instance_params) else: func = ch_func_to_all_pk(func) func_param_keys = signature(func).parameters.keys() self_argnames = func_param_keys & instance_params method_argnames = func_param_keys - self_argnames def method(self, **kwargs): kwargs_from_self = {a: getattr(self, a) for a in self_argnames} kwargs.update(kwargs_from_self) return func(**kwargs) set_signature_of_func(method, ['self'] + list(method_argnames)) method.__name__ = func.__name__ return method
def mk_request_function(method_spec, *, function_kind='method', dispatch=request): """ Makes function that will make http requests for you, on your own terms. Specify what API you want to talk to, and how you want to talk to it (and it talk back to you), and get a function that does exactly that. Essentially, this factory function allows you to take an API specification, specify how you want it to relate to python objects (how to convert input arguments to API elements, and how to convert the response of the request, for instance), and get a method that is ready to be used. What does a method_spec contain? Well... consider first this. The core of this code is this: ``` request_kwargs = dict(**method_spec['request_kwargs']) # to make a copy ... # a bunch more code that updates request_kwargs according to other keys of method_spec ... # ... and some other debugging hooks r = request(**request_kwargs) # call the request if 'output_trans' in method_spec: # see if there's an output_trans function r = method_spec['output_trans'](r) # ... if so, use it to extract what you want from the response return r ``` So you can do almost everything you need with one single key: `'request_kwargs'` and `'output_trans'` alone. But there wouldn't be as much advantage over just calling requests if that's all there was to it, so we offer some other special keys to cover some of the common patterns. - 'method': - 'url_template': Specify the url, but with named placeholders: Example `'http://base.com/{user}/{action}'`. - 'json_arg_names': Specify the names of arguments of the function that should be put in the json load - 'debug': 'print_request_kwargs' or 'return_request_kwargs' - 'input_trans': Function applied to - 'output_trans': A function applied to response object to extract what we want to return. - 'wraps': A function whose signature we should use as the output's function's signature :param method_spec: Specification of how to convert arguments of the function that is being made to an http request. :return: A function. Note: I say "function", but the function is meant to be a method, so the function has a self as first argument. That argument is ignored. """ original_spec = method_spec method_spec = method_spec.copy() # make a copy method_spec['input_trans'] = method_spec.get('input_trans', None) or {} request_kwargs = method_spec.get('request_kwargs', {}).copy() method = method_spec.pop('method', request_kwargs.pop('method', DFLT_REQUEST_METHOD)) path_arg_names = _ensure_list(method_spec.get('path_arg_names', [])) query_arg_names = _ensure_list(method_spec.get('query_arg_names', [])) body_arg_names = _ensure_list(method_spec.get('body_arg_names', [])) arg_specs = method_spec.get('arg_specs', []) formatted_arg_specs = [ mk_param_spec_from_arg_schema(arg) for arg in arg_specs ] func_args = path_arg_names + query_arg_names + body_arg_names debug = method_spec.pop('debug', None) if 'debug' in method_spec: debug = method_spec['debug'] output_trans = method_spec.pop('output_trans', None) if output_trans is None: response_type = method_spec.get('response_type', 'text/plain') if response_type == 'text/plain': output_trans = default_text_output_trans if response_type == 'application/json': output_trans = default_json_output_trans else: output_trans = default_content_output_trans wraps_func = method_spec.pop('wraps', None) # TODO: inject a signature, and possibly a __doc__ in this function def request_func(*args, **kwargs): def get_req_param_key(): content_type = method_spec.get('content_type', 'text/plain') if content_type == 'text/plain': return 'data' if content_type == 'multipart/form-data': return 'files' if content_type == 'application/octet-stream': return 'stream' kwargs = dict( kwargs, **{argname: argval for argname, argval in zip(func_args, args)}, ) # convert argument types TODO: Not efficient. Might could be revised. for arg_name, converter in method_spec.get('input_trans', {}).items(): if arg_name in kwargs: kwargs[arg_name] = converter(kwargs[arg_name]) # making the request_kwargs #################################################################################### _request_kwargs = dict(**request_kwargs) # to make a copy url = None if 'url_template' in method_spec: url_template = method_spec['url_template'] # Check if the url template already have parameters, add them otherwise if query_arg_names and not re.search('^.*\?((.*=.*)(&?))+$', url_template): url_arg_parts = [ f'{x}={{{x}}}' for x in query_arg_names if x in kwargs ] url_args = '&'.join(url_arg_parts) if url_args: url_template += f'?{url_args}' param_kwargs = { k: v for k, v in kwargs.items() if k in path_arg_names or k in query_arg_names } encoded_kwargs = { k: urllib.parse.quote(v, safe='') if type(v) is str else v for k, v in param_kwargs.items() } url = url_template.format(**encoded_kwargs) elif 'url' in method_spec: url = method_spec['url'] json = {k: v for k, v in kwargs.items() if is_jsonable(v)} _request_kwargs['json'] = json remaining_kwargs = {k: v for k, v in kwargs.items() if k not in json} if remaining_kwargs: req_param_key = get_req_param_key() _request_kwargs[req_param_key] = { k: v for k, v in remaining_kwargs.items() if k in body_arg_names } if debug is not None: if debug == 'print_request_kwargs': print(_request_kwargs) elif debug == 'return_request_kwargs': return _request_kwargs r = dispatch(method, url, **_request_kwargs) if callable(output_trans): return output_trans(r) return r if function_kind == 'method': _request_func = request_func def request_func(self, *args, **kwargs): return _request_func(*args, **kwargs) if wraps_func: return wraps(wraps_func)(request_func) else: if formatted_arg_specs: if function_kind == 'method': set_signature_of_func(request_func, ['self'] + formatted_arg_specs) elif function_kind == 'function': set_signature_of_func(request_func, formatted_arg_specs) request_func.original_spec = original_spec request_func.func_args = func_args request_func.debug = debug request_func.method_spec = method_spec funcname = method_spec.get('method_name', None) if funcname: request_func.__name__ = funcname docstring = method_spec.get('docstring', None) if docstring: request_func.__doc__ = docstring assert callable( output_trans ), f'output_trans {output_trans} is not callable, try again' return request_func
def mk_request_function(method_spec): """ Makes function that will make http requests for you, on your own terms. Specify what API you want to talk to, and how you want to talk to it (and it talk back to you), and get a function that does exactly that. Essentially, this factory function allows you to take an API specification, specify how you want it to relate to python objects (how to convert input arguments to API elements, and how to convert the response of the request, for instance), and get a method that is ready to be used. :param method_spec: Specification of how to convert arguments of the function that is being made to an http request. :return: A function. Note: I say "function", but the function is meant to be a method, so the function has a self as first argument. That argument is ignored. """ # defaults method_spec = method_spec.copy() method_spec['request_kwargs'] = method_spec.get('request_kwargs', {}) method_spec['request_kwargs']['method'] = method_spec[ 'request_kwargs'].get('method', 'GET') arg_order = _ensure_list(method_spec.get('args', [])) # TODO: inject a signature, and possibly a __doc__ in this function def request_func(self, *args, **kwargs): # absorb args in kwargs if len(args) > len(arg_order): raise ValueError( f"The number ({len(args)}) of unnamed arguments was greater than " f"the number ({len(arg_order)}of specified arguments in arg_order" ) kwargs = dict( kwargs, **{argname: argval for argname, argval in zip(arg_order, args)}) # convert argument types TODO: Not efficient. Might could be revised. for arg_name, converter in method_spec.get('input_trans', {}).items(): if arg_name in kwargs: kwargs[arg_name] = converter(kwargs[arg_name]) json_data = {} for arg_name in method_spec.get('json_arg_names', []): if arg_name in kwargs: json_data[arg_name] = kwargs.pop(arg_name) # making the request_kwargs #################################################################################### request_kwargs = method_spec['request_kwargs'] if 'url_template' in method_spec: request_kwargs['url'] = method_spec['url_template'].format( **kwargs) elif 'url' in method_spec: request_kwargs['url'] = method_spec['url'] if json_data: request_kwargs['json'] = json_data if 'debug' in method_spec: debug = method_spec['debug'] if debug == 'print_request_kwargs': print(request_kwargs) elif debug == 'return_request_kwargs': return request_kwargs r = request(**request_kwargs) if 'output_trans' in method_spec: r = method_spec['output_trans'](r) return r if 'wraps' in method_spec: return wraps(method_spec['wraps'])(request_func) else: all_args = method_spec.get('args', []) + method_spec.get( 'json_arg_names', []) if all_args: set_signature_of_func(request_func, ['self'] + all_args) return request_func
register_cli_method(openapi_spec, method, expected_auth_kwargs) for methodname, method in client_details.__dict__.items() if getattr(method, 'method_spec', None) ] parser = argh.ArghParser() parser.add_commands(cli_methods) return parser def dispatch_cli(*args, **kwargs): """Makes a CLI parser with mk_cli and then dispatches it (see documentation of mk_cli)""" parser = mk_cli(*args, **kwargs) parser.dispatch() set_signature_of_func(dispatch_cli, signature(mk_cli)) def register_cli_method( openapi_spec: dict, client_method: Callable, expected_auth_kwargs: Iterable[str] = None, config_filename: str = DFLT_CONFIG_FILENAME, profile: str = '', ): """Creates a CLI-friendly function to instantiate an HttpClient with appropriate authentication arguments and call a particular method of the client instance :param openapi_spec: The OpenAPI spec used to make the client :param client_method: The instance method to wrap :param expected_auth_kwargs: A list of authentication kwargs that the CLI should ask for
def mk_request_function(method_spec): """ Makes function that will make http requests for you, on your own terms. Specify what API you want to talk to, and how you want to talk to it (and it talk back to you), and get a function that does exactly that. Essentially, this factory function allows you to take an API specification, specify how you want it to relate to python objects (how to convert input arguments to API elements, and how to convert the response of the request, for instance), and get a method that is ready to be used. What does a method_spec contain? Well... consider first this. The core of this code is this: ``` request_kwargs = dict(**method_spec['request_kwargs']) # to make a copy ... # a bunch more code that updates request_kwargs according to other keys of method_spec ... # ... and some other debugging hooks r = request(**request_kwargs) # call the request if 'output_trans' in method_spec: # see if there's an output_trans function r = method_spec['output_trans'](r) # ... if so, use it to extract what you want from the response return r ``` So you can do almost everything you need with one single key: `'request_kwargs'` and `'output_trans'` alone. But there wouldn't be as much advantage over just calling requests if that's all there was to it, so we offer some other special keys to cover some of the common patterns. - 'method': - 'url_template': Specify the url, but with named placeholders: Example `'http://base.com/{user}/{action}'`. - 'json_arg_names': Specify the names of arguments of the function that should be put in the json load - 'debug': 'print_request_kwargs' or 'return_request_kwargs' - 'input_trans': Function applied to - 'output_trans': A function applied to response object to extract what we want to return. - 'wraps': A function whose signature we should use as the output's function's signature :param method_spec: Specification of how to convert arguments of the function that is being made to an http request. :return: A function. Note: I say "function", but the function is meant to be a method, so the function has a self as first argument. That argument is ignored. """ # defaults original_spec = method_spec method_spec = method_spec.copy() # make a copy request_kwargs = method_spec.get('request_kwargs', {}).copy() method = method_spec.pop('method', request_kwargs.pop('method', DFLT_REQUEST_METHOD)) func_args = _ensure_list(method_spec.get('args', [])) + _ensure_list( method_spec.get('json_arg_names', [])) debug = method_spec.pop('debug', None) if 'debug' in method_spec: debug = method_spec['debug'] output_trans = method_spec.pop('output_trans', lambda x: x) wraps_func = method_spec.pop('wraps', None) # TODO: inject a signature, and possibly a __doc__ in this function def request_func(self, *args, **kwargs): kwargs = dict( kwargs, **{argname: argval for argname, argval in zip(func_args, args)}) # convert argument types TODO: Not efficient. Might could be revised. for arg_name, converter in method_spec.get('input_trans', {}).items(): if arg_name in kwargs: kwargs[arg_name] = converter(kwargs[arg_name]) json_data = {} for arg_name in method_spec.get('json_arg_names', []): if arg_name in kwargs: json_data[arg_name] = kwargs.pop(arg_name) # making the request_kwargs #################################################################################### _request_kwargs = dict(**request_kwargs) # to make a copy url = None if 'url_template' in method_spec: url = method_spec['url_template'].format(**kwargs) elif 'url' in method_spec: url = method_spec.pop('url', None) if json_data: _request_kwargs['json'] = json_data if debug is not None: if debug == 'print_request_kwargs': print(_request_kwargs) elif debug == 'return_request_kwargs': return _request_kwargs r = request(method, url, **_request_kwargs) return output_trans(r) if wraps_func: return wraps(wraps_func)(request_func) else: if func_args: set_signature_of_func(request_func, ['self'] + func_args) request_func.original_spec = original_spec request_func.func_args = func_args request_func.debug = debug request_func.method_spec = method_spec return request_func