Reputation: 842
I have a REST api and want to write a wrapper around it in Python for others to use. It's a search api and each parameters are treated as AND
Example:
api/search/v1/search_parameters[words]=cat cute fluffy&search_parameters[author_id]=12345&search_parameters[filter][orientation]=horizontal
What's the most Pythonic way to write a function that takes all this arguments, must specify at least one search_parameters string
and value
.
My wrapper function would look something like this below but I'm lost with the way the user can input multiple search parameter for this search api call:
def search(self):
url = BASE_URL + search_param_url
response = self.session.get(url)
return response.json()
In the end, users should be able to just call something like api.search()
Upvotes: 0
Views: 137
Reputation: 9664
Disclaimer: questions like what is the most Pythonic (best/prettiest) way can attract unnecessary discussion (and create a distraction) yielding an inconclusive results. My personal recommendation, over reusing recommendation from a particular part of the community would be above all: be consistent across your code and how you design your interfaces. Think of those who will use them (incl. yourself 12 months down the road). As well as "The Best" solution is usually function of the intended purpose and not necessarily a universal constant (even though there might be more or less recommendable ways). That said.
If I understand correctly, your parameters are of key=value
pairs nature (and you will expand them into URL as search_parameters[key]=value
). Event though the filter
and orientation
in your example throw me off... if not true, please, describe a bit more and I can revisit my suggestion. For that a dictionary seems to offer itself as a good choice. To get one, your method could be either:
def search(self, search_kwargs):
...
And you expect your user to pass a dict of parameters (args_dict = {'string': 'xxx', ...}; c.search(args_dict)
). Or:
def search(self, **kwargs):
...
And you expect your user to pass key/value pairs as keyword arguments of the method (c.search(string='xxx')
). I would probably favor the former option. Dict is flexible when you prepare the parameters (and yes, you could also pass a dict in the latter case, but that kind beats the purpose of keyword arguments expansion; always chose the simpler option achieving the same goal).
In any case, you can just take the dict (my_args
stands for either one of the two above). Check you have at least one of the required keys:
not ('string' in my_args or 'value' in my_args):
raise SearchParamsError("Require 'string' or 'value'.")
Perform any other sanity checks. Prepare params to be appended to the URL:
url_params = '&'.join(('{}={}'.format(k, my_dict[k]) for k in my_dict))
That's the trivial stuff. But depending on your needs and usage, you may actually introduce a (e.g.) SearchRequest
class whose constructor could take initial set of parameters similar to the above described method, but you would have further method(s) allowing to manipulate the search (add more parameters) before executing it. And each parameter addition could be already subject to validity check. You could make the instance callable to execute the search itself (corresponding method) or pass this to a search method that takes a prepared requests as its argument.
Updated based on bit more insight in the comment.
If your API actually uses (arbitrarily) nested mapping objects, dictionary is still a good structure to hold your parameters. I'd pick one of the two options.
You can use nested dictionaries, which might afford you flexibility describing the request and could more accurately reflect how your REST API understand its data -> the way you form your request is more similar to how the REST API describes it. However using keyword arguments mentioned above is no longer an option (or not without extra work similar to the next option and some more translation). And the structure of the data might make (esp. simple cases) using it less convenient. E.g.:
my_dict = {'string': 'foo bar',
'author_id': 12345,
'filter': {'orientation': 'horizontal',
'other': 'baz'},
'other': {'more': {'nested': 1,
'also': 2},
'less': 'flat'}}
def par_dict_format(in_dict, *, _pfx='search_parameters'):
ret = []
for key, value in in_dict.items():
if isinstance(value, dict):
ret.append(par_dict_format(value, _pfx='{}[{}]'.format(_pfx, key)))
else:
ret.append('{}[{}]={}'.format(_pfx, key, value))
return '&'.join(ret)
Or you can opt for a structure of flat key/value pairs introducing notation using reasonable and non-conflicting separator for individual elements. Depending on the separator used, you could even get keyword arguments back into play (not with the .
in my example though). One of the downsides is, you effectively create a new/parallel interface and notation. E.g.:
my_dict = {'string': 'foo bar',
'author_id': 12345,
'filter.orientation': 'horizontal',
'filter.other': 'baz',
'other.more.nested': 1,
'other.more.also': 2,
'other.more.also': 2,
'other.less': 'flat'}
def par_dict_format(in_dict):
ret = []
for key, value in in_dict.items():
key_str = ''.join(('[{}]'.format(p) for p in key.split('.')))
ret.append('{}={}'.format(key_str, value))
return '&'.join(('search_parameters{}'.format(i) for i in ret))
My take on these two would be. If I mostly construct the query programmatically (for instance having different methods to launch different queries), I'd lean to nesting dictionaries. If expected usage would be geared more towards people writing queries directly, calling the search
method or even perhaps exposing it through a CLI, the latter (flat) structure could be easier to use/write for that.
Upvotes: 1