-
Notifications
You must be signed in to change notification settings - Fork 287
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #410 from tstrachota/cli_generator
CLI generator
- Loading branch information
Showing
6 changed files
with
442 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
|
||
KATELLO CLI GENERATOR | ||
===================== | ||
|
||
Description: | ||
|
||
This utility semi-automatically generates python code for Katello CLI | ||
based on json exports from apipie [1] documentation tool. | ||
|
||
It generates: | ||
* python bindings for the documented api (goes into: /katello/client/api/<RESOURCE_NAME>.py) | ||
* code for cli command (goes into: /katello/client/core/<RESOURCE_NAME>.py) | ||
* code frame for actions (goes into: --^ ) | ||
* code for wiring commands and actions into the cli (goes into: /katello/client/main.py) | ||
|
||
The tool requires python-mako [2] and python-inflector templating library installed [3]. | ||
The latter is available in our nightly katello repo. | ||
|
||
[1] https://github.com/Pajk/apipie-rails/ | ||
[2] http://www.makotemplates.org/ | ||
[3] https://github.com/bermi/Python-Inflector | ||
|
||
Options: | ||
|
||
-h, --help show this help message and exit | ||
--binding, --api generate python api bindings | ||
--command generate cli command frame | ||
--action generate cli action for a method | ||
--main generate code for wiring commands and actions into cli | ||
-r RESOURCE, --resource=RESOURCE | ||
-m METHOD, --method=METHOD | ||
-i INPUT, --input=INPUT | ||
input file with json apipie documentation export | ||
|
||
|
||
Example usage: | ||
|
||
curl http://foreman-rhel:3000/apidoc.json | ./generate.py --resource operatingsystems --api | ||
curl http://foreman-rhel:3000/apidoc.json | ./generate.py --resource operatingsystems --command | ||
curl http://foreman-rhel:3000/apidoc.json | ./generate.py --resource operatingsystems --method index --action | ||
curl http://foreman-rhel:3000/apidoc.json | ./generate.py --resource operatingsystems --main | ||
|
||
or | ||
|
||
wget http://foreman-rhel:3000/apidoc.json | ||
./generate.py --resource operatingsystems --api -i ./apidoc.json | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
#!/usr/bin/python | ||
# | ||
# Katello Shell | ||
# Copyright (c) 2010 Red Hat, Inc. | ||
# | ||
# This software is licensed to you under the GNU General Public License, | ||
# version 2 (GPLv2). There is NO WARRANTY for this software, express or | ||
# implied, including the implied warranties of MERCHANTABILITY or FITNESS | ||
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 | ||
# along with this software; if not, see | ||
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. | ||
# | ||
# Red Hat trademarks are not licensed under GPLv2. No permission is | ||
# granted to use or replicate Red Hat trademarks that are incorporated | ||
# in this software or its documentation. | ||
# | ||
|
||
import os | ||
import sys | ||
import json | ||
import re | ||
import codecs | ||
from mako.template import Template | ||
from optparse import OptionParser | ||
from python_inflector.inflector import Inflector, English | ||
|
||
class MethodDocGenerator(object): | ||
|
||
def __params_doc(self, params, index_prefix=None): | ||
doc = [] | ||
for param in params: | ||
doc += self.__single_param_doc(param, index_prefix) | ||
return doc | ||
|
||
def __single_param_doc(self, param, index_prefix=None): | ||
prefix = index_prefix if index_prefix else "" | ||
|
||
doc = [] | ||
str_index = prefix +"['"+ param.name() +"']" | ||
doc += [":type data"+ str_index +": "+ param.expected_type()] | ||
doc += [":param data"+ str_index +": "+ param.description()] | ||
if param.inner_params(): | ||
doc += self.__params_doc(param.inner_params(), str_index) | ||
return doc | ||
|
||
def generate(self, method, indent=""): | ||
doc = [method.description(), ""] | ||
doc += self.__params_doc(method.params()) | ||
return indent + ("\n"+indent).join(doc).strip() | ||
|
||
|
||
class Param(object): | ||
|
||
def __init__(self, json): | ||
self.json = json | ||
|
||
def name(self): | ||
return self.json['name'] | ||
|
||
def expected_type(self): | ||
return self.json['expected_type'] | ||
|
||
def description(self): | ||
return self.json['description'] | ||
|
||
def required(self): | ||
return (self.json['required'] == True) | ||
|
||
def help(self): | ||
desc = self.json['description'] | ||
if self.required(): | ||
desc += ' (required)' | ||
return desc | ||
|
||
def inner_params(self): | ||
return [ Param(p) for p in self.json.get('params', []) ] | ||
|
||
|
||
class Method(object): | ||
|
||
PATH_PARAM_RE = r":([^/]+)" | ||
|
||
def __init__(self, json, resource_name=""): | ||
self.json = json | ||
self.__resource_name = resource_name | ||
|
||
def __json_url(self): | ||
return self.json['apis'][0]['api_url'] | ||
|
||
def __get_path_params(self): | ||
return re.findall(self.PATH_PARAM_RE, self.__json_url()) | ||
|
||
def path(self): | ||
url = re.sub(self.PATH_PARAM_RE, "%s", self.__json_url()) | ||
url = '"'+ url +'"' | ||
if self.__get_path_params(): | ||
url += ' % ('+ ', '.join(self.__get_path_params()) +')' | ||
return url | ||
|
||
def arguments(self): | ||
args = ["self"] | ||
args += self.__get_path_params() | ||
if self.accepts_data(): | ||
args += [self.data_var_name()] | ||
return args | ||
|
||
def data_var_name(self): | ||
if self.http_method() == 'GET': | ||
return 'queries' | ||
else: | ||
return 'data' | ||
|
||
def data_keys(self): | ||
return (p.name() for p in self.params()) | ||
|
||
def accepts_data(self): | ||
return len(self.json['params']) > 0 | ||
|
||
def name(self, safe=False, title=False): | ||
if self.json['name'].lower() == 'index': | ||
name = 'list' | ||
else: | ||
name = self.json['name'] | ||
if title: | ||
name = name.title() | ||
return name.replace(" ", "") if safe else name | ||
|
||
def description(self): | ||
desc = self.json['full_description'] | ||
if not desc: | ||
desc = self.name() +" "+ self.__resource_name | ||
return desc | ||
|
||
def http_method(self): | ||
return self.json['apis'][0]['http_method'] | ||
|
||
def __params(self): | ||
return [ Param(p) for p in self.json.get('params', []) ] | ||
|
||
def __unnest_params(self, params): | ||
if self.__can_unnest(params): | ||
params = params[0].inner_params() | ||
return params | ||
|
||
def __can_unnest(self, params): | ||
return (len(params) == 1 and params[0].expected_type() == 'hash') | ||
|
||
def param_nest(self): | ||
if not self.__can_unnest(self.__params()): | ||
return None | ||
else: | ||
return self.__params()[0] | ||
|
||
def params(self, required=False): | ||
params = self.__unnest_params(self.__params()) | ||
if required: | ||
params = [p for p in params if p.required()] | ||
return params | ||
|
||
|
||
class Resource(object): | ||
|
||
def __init__(self, json): | ||
self.json = json | ||
self.inflector = Inflector(English) | ||
|
||
def get_method(self, name): | ||
name_dict = dict((m["name"], m) for m in self.json.get('methods', [])) | ||
return Method(name_dict[name], self.name()) | ||
|
||
def has_method(self, name): | ||
return name in [m['name'] for m in self.json.get('methods', [])] | ||
|
||
def methods(self): | ||
return [Method(m, self.name()) for m in self.json.get('methods', [])] | ||
|
||
def name(self, safe=False, title=False): | ||
name = self.inflector.singularize(self.json['name']).lower() | ||
if title: | ||
name = name.title() | ||
return name.replace(" ", "") if safe else name | ||
|
||
|
||
|
||
|
||
|
||
def load_json(filename): | ||
if filename: | ||
with open(filename, 'r') as f: | ||
content = f.read() | ||
else: | ||
content = sys.stdin.read() | ||
return json.loads(content) | ||
|
||
|
||
def generate_action(resource, method_name): | ||
mytemplate = Template(filename='./templates/action.py') | ||
print mytemplate.render(resource=resource, method=resource.get_method(method_name)) | ||
|
||
def generate_command(resource): | ||
mytemplate = Template(filename='./templates/command.py') | ||
print mytemplate.render(resource=resource, name=resource.name()) | ||
|
||
def generate_binding(resource): | ||
mytemplate = Template(filename='./templates/api.py') | ||
print mytemplate.render(resource=resource, doc=MethodDocGenerator()) | ||
|
||
def generate_main(resource): | ||
mytemplate = Template(filename='./templates/main.py') | ||
print mytemplate.render(resource=resource, name=resource.name(safe=True)) | ||
|
||
|
||
def generate(): | ||
parser = OptionParser() | ||
parser.add_option("--binding", "--api", action="store_true", help="generate python api bindings") | ||
parser.add_option("--command", action="store_true", help="generate cli command frame") | ||
parser.add_option("--action", action="store_true", help="generate cli action for a method") | ||
parser.add_option("--main", action="store_true", help="generate code for wiring commands and actions into cli") | ||
parser.add_option("-r", "--resource") | ||
parser.add_option("-m", "--method") | ||
parser.add_option("-i", "--input", help="input file with json apipie documentation export") | ||
opts, args = parser.parse_args(sys.argv[1:]) | ||
|
||
j = load_json(getattr(opts, 'input')) | ||
try: | ||
resource_json = j['docs']['resources'][getattr(opts, 'resource')] | ||
except KeyError, e: | ||
print >> sys.stderr, "Invalid resource. Choose one of: "+ ", ".join(j['docs']['resources'].keys()) | ||
exit(1) | ||
resource = Resource(resource_json) | ||
|
||
if getattr(opts, 'binding'): | ||
generate_binding(resource) | ||
elif getattr(opts, 'command'): | ||
generate_command(resource) | ||
elif getattr(opts, 'action'): | ||
method_name = getattr(opts, 'method') | ||
if not resource.has_method(method_name): | ||
print >> sys.stderr, "Invalid method. Choose one of: "+ ", ".join([m['name'] for m in resource.json.get('methods', [])]) | ||
exit(1) | ||
generate_action(resource, getattr(opts, 'method')) | ||
elif getattr(opts, 'main'): | ||
generate_main(resource) | ||
else: | ||
print >> sys.stderr, "You have to choose some action" | ||
|
||
|
||
|
||
|
||
|
||
if __name__ == "__main__": | ||
# Change encoding of output streams when no encoding is forced via $PYTHONIOENCODING | ||
# or setting in lib/python{version}/site-packages | ||
if sys.getdefaultencoding() == 'ascii': | ||
writer_class = codecs.getwriter('utf-8') | ||
if sys.stdout.encoding == None: | ||
sys.stdout = writer_class(sys.stdout) | ||
if sys.stderr.encoding == None: | ||
sys.stderr = writer_class(sys.stderr) | ||
|
||
generate() | ||
sys.exit(os.EX_OK) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
|
||
class ${method.name(True, True)}(${resource.name(True, True)}Action): | ||
|
||
description = _('${method.description()}') | ||
|
||
def setup_parser(self, parser): | ||
% for p in method.params(): | ||
parser.add_option('--${p.name()}', dest='${p.name()}', help=_("${p.help()}")) | ||
% endfor | ||
% if method.name() in ['destroy', 'show']: | ||
parser.add_option('--name', dest='name', help=_(" (required)")) | ||
% endif | ||
|
||
def check_options(self, validator): | ||
% if method.params(required=True): | ||
validator.require('${"', '".join([p.name() for p in method.params(required=True)])}') | ||
% else: | ||
#validator.require() | ||
% endif | ||
#TODO: fill the method body | ||
pass | ||
|
||
def run(self): | ||
#TODO: fill the method body | ||
|
||
% if method.name() in ['list', 'show'] and resource.has_method('create'): | ||
${resource.name(True, False)} = self.api.${method.name(True, False)}() | ||
% for p in resource.get_method('create').params(): | ||
self.printer.add_column('${p.name()}') | ||
% endfor | ||
|
||
#TODO: print the data | ||
self.printer.set_header(_("${resource.name(False, True)}")) | ||
self.printer.print_item(${resource.name(True, False)}) | ||
% else: | ||
${resource.name(True)} = self.api.${method.name(True)}() | ||
print _('${resource.name(False, True)} ...') | ||
% endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright © 2011 Red Hat, Inc. | ||
# | ||
# This software is licensed to you under the GNU General Public License, | ||
# version 2 (GPLv2). There is NO WARRANTY for this software, express or | ||
# implied, including the implied warranties of MERCHANTABILITY or FITNESS | ||
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 | ||
# along with this software; if not, see | ||
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. | ||
# | ||
# Red Hat trademarks are not licensed under GPLv2. No permission is | ||
# granted to use or replicate Red Hat trademarks that are incorporated | ||
# in this software or its documentation. | ||
|
||
from katello.client.api.base import KatelloAPI | ||
|
||
|
||
def slice_dict(d, *key_list): | ||
default = None | ||
return dict((k, d.get(k, default)) for k in key_list) | ||
|
||
class ${resource.name(True, True)}API(KatelloAPI): | ||
|
||
% for m in resource.methods(): | ||
|
||
def ${m.name(True)}(${", ".join(m.arguments())}): | ||
""" | ||
${doc.generate(m, " "*8)} | ||
""" | ||
path = ${m.path()} | ||
% if m.accepts_data(): | ||
${m.data_var_name()} = slice_dict(${m.data_var_name()}, ${", ".join(["'"+k+"'" for k in m.data_keys()] )}) | ||
% if m.param_nest(): | ||
return self.server.${m.http_method()}(path, {"${m.param_nest().name()}": ${m.data_var_name()}})[1] | ||
% else: | ||
return self.server.${m.http_method()}(path, ${m.data_var_name()})[1] | ||
% endif | ||
% else: | ||
return self.server.${m.http_method()}(path)[1] | ||
% endif | ||
|
||
% endfor |
Oops, something went wrong.