Skip to content

Commit

Permalink
[v4] CLI support is back
Browse files Browse the repository at this point in the history
  • Loading branch information
Gauvain Pocentek committed Aug 17, 2017
1 parent a4f0c52 commit 9783207
Show file tree
Hide file tree
Showing 4 changed files with 407 additions and 6 deletions.
7 changes: 7 additions & 0 deletions gitlab/base.py
Expand Up @@ -607,6 +607,13 @@ def get_id(self):
return None
return getattr(self, self._id_attr)

@property
def attributes(self):
d = self.__dict__['_updated_attrs'].copy()
d.update(self.__dict__['_attrs'])
d.update(self.__dict__['_parent_attrs'])
return d


class RESTObjectList(object):
"""Generator object representing a list of RESTObject's.
Expand Down
41 changes: 39 additions & 2 deletions gitlab/cli.py
Expand Up @@ -17,8 +17,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from __future__ import print_function
from __future__ import absolute_import
import argparse
import functools
import importlib
import re
import sys
Expand All @@ -27,6 +27,36 @@

camel_re = re.compile('(.)([A-Z])')

# custom_actions = {
# cls: {
# action: (mandatory_args, optional_args, in_obj),
# },
# }
custom_actions = {}


def register_custom_action(cls_name, mandatory=tuple(), optional=tuple()):
def wrap(f):
@functools.wraps(f)
def wrapped_f(*args, **kwargs):
return f(*args, **kwargs)

# in_obj defines whether the method belongs to the obj or the manager
in_obj = True
final_name = cls_name
if cls_name.endswith('Manager'):
final_name = cls_name.replace('Manager', '')
in_obj = False
if final_name not in custom_actions:
custom_actions[final_name] = {}

action = f.__name__

custom_actions[final_name][action] = (mandatory, optional, in_obj)

return wrapped_f
return wrap


def die(msg, e=None):
if e:
Expand All @@ -51,6 +81,9 @@ def _get_base_parser():
parser.add_argument("-v", "--verbose", "--fancy",
help="Verbose mode",
action="store_true")
parser.add_argument("-d", "--debug",
help="Debug mode (display HTTP requests",
action="store_true")
parser.add_argument("-c", "--config-file", action='append',
help=("Configuration file to use. Can be used "
"multiple times."))
Expand Down Expand Up @@ -84,12 +117,13 @@ def main():
config_files = args.config_file
gitlab_id = args.gitlab
verbose = args.verbose
debug = args.debug
action = args.action
what = args.what

args = args.__dict__
# Remove CLI behavior-related args
for item in ('gitlab', 'config_file', 'verbose', 'what', 'action',
for item in ('gitlab', 'config_file', 'verbose', 'debug', 'what', 'action',
'version'):
args.pop(item)
args = {k: v for k, v in args.items() if v is not None}
Expand All @@ -100,6 +134,9 @@ def main():
except Exception as e:
die(str(e))

if debug:
gl.enable_debug()

cli_module.run(gl, what, action, args, verbose)

sys.exit(0)
296 changes: 296 additions & 0 deletions gitlab/v4/cli.py
@@ -0,0 +1,296 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from __future__ import print_function
import inspect
import operator

import six

import gitlab
import gitlab.base
from gitlab import cli
import gitlab.v4.objects


class GitlabCLI(object):
def __init__(self, gl, what, action, args):
self.cls_name = cli.what_to_cls(what)
self.cls = gitlab.v4.objects.__dict__[self.cls_name]
self.what = what.replace('-', '_')
self.action = action.lower().replace('-', '')
self.gl = gl
self.args = args
self.mgr_cls = getattr(gitlab.v4.objects,
self.cls.__name__ + 'Manager')
# We could do something smart, like splitting the manager name to find
# parents, build the chain of managers to get to the final object.
# Instead we do something ugly and efficient: interpolate variables in
# the class _path attribute, and replace the value with the result.
self.mgr_cls._path = self.mgr_cls._path % self.args
self.mgr = self.mgr_cls(gl)

def __call__(self):
method = 'do_%s' % self.action
if hasattr(self, method):
return getattr(self, method)()
else:
return self.do_custom()

def do_custom(self):
in_obj = cli.custom_actions[self.cls_name][self.action][2]

# Get the object (lazy), then act
if in_obj:
data = {}
if hasattr(self.mgr, '_from_parent_attrs'):
for k in self.mgr._from_parent_attrs:
data[k] = self.args[k]
if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.cls):
data[self.cls._id_attr] = self.args.pop(self.cls._id_attr)
o = self.cls(self.mgr, data)
return getattr(o, self.action)(**self.args)
else:
return getattr(self.mgr, self.action)(**self.args)

def do_create(self):
try:
return self.mgr.create(self.args)
except Exception as e:
cli.die("Impossible to create object", e)

def do_list(self):
try:
return self.mgr.list(**self.args)
except Exception as e:
cli.die("Impossible to list objects", e)

def do_get(self):
id = None
if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.cls):
id = self.args.pop(self.cls._id_attr)

try:
return self.mgr.get(id, **self.args)
except Exception as e:
cli.die("Impossible to get object", e)

def do_delete(self):
id = self.args.pop(self.cls._id_attr)
try:
self.mgr.delete(id, **self.args)
except Exception as e:
cli.die("Impossible to destroy object", e)

def do_update(self):
id = self.args.pop(self.cls._id_attr)
try:
return self.mgr.update(id, self.args)
except Exception as e:
cli.die("Impossible to update object", e)


def _populate_sub_parser_by_class(cls, sub_parser):
mgr_cls_name = cls.__name__ + 'Manager'
mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)

for action_name in ['list', 'get', 'create', 'update', 'delete']:
if not hasattr(mgr_cls, action_name):
continue

sub_parser_action = sub_parser.add_parser(action_name)
if hasattr(mgr_cls, '_from_parent_attrs'):
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=True)
for x in mgr_cls._from_parent_attrs]
sub_parser_action.add_argument("--sudo", required=False)

if action_name == "list":
if hasattr(mgr_cls, '_list_filters'):
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=False)
for x in mgr_cls._list_filters]

sub_parser_action.add_argument("--page", required=False)
sub_parser_action.add_argument("--per-page", required=False)
sub_parser_action.add_argument("--all", required=False,
action='store_true')

if action_name == 'delete':
id_attr = cls._id_attr.replace('_', '-')
sub_parser_action.add_argument("--%s" % id_attr, required=True)

if action_name == "get":
if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(cls):
if cls._id_attr is not None:
id_attr = cls._id_attr.replace('_', '-')
sub_parser_action.add_argument("--%s" % id_attr,
required=True)

if hasattr(mgr_cls, '_optional_get_attrs'):
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=False)
for x in mgr_cls._optional_get_attrs]

if action_name == "create":
if hasattr(mgr_cls, '_create_attrs'):
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=True)
for x in mgr_cls._create_attrs[0] if x != cls._id_attr]

[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=False)
for x in mgr_cls._create_attrs[1] if x != cls._id_attr]

if action_name == "update":
if cls._id_attr is not None:
id_attr = cls._id_attr.replace('_', '-')
sub_parser_action.add_argument("--%s" % id_attr,
required=True)

if hasattr(mgr_cls, '_update_attrs'):
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=True)
for x in mgr_cls._update_attrs[0] if x != cls._id_attr]

[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=False)
for x in mgr_cls._update_attrs[1] if x != cls._id_attr]

if cls.__name__ in cli.custom_actions:
name = cls.__name__
for action_name in cli.custom_actions[name]:
sub_parser_action = sub_parser.add_parser(action_name)
# Get the attributes for URL/path construction
if hasattr(mgr_cls, '_from_parent_attrs'):
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=True)
for x in mgr_cls._from_parent_attrs]
sub_parser_action.add_argument("--sudo", required=False)

# We need to get the object somehow
if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(cls):
if cls._id_attr is not None:
id_attr = cls._id_attr.replace('_', '-')
sub_parser_action.add_argument("--%s" % id_attr,
required=True)

required, optional, dummy = cli.custom_actions[name][action_name]
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=True)
for x in required if x != cls._id_attr]
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=False)
for x in optional if x != cls._id_attr]

if mgr_cls.__name__ in cli.custom_actions:
name = mgr_cls.__name__
for action_name in cli.custom_actions[name]:
sub_parser_action = sub_parser.add_parser(action_name)
if hasattr(mgr_cls, '_from_parent_attrs'):
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=True)
for x in mgr_cls._from_parent_attrs]
sub_parser_action.add_argument("--sudo", required=False)

required, optional, dummy = cli.custom_actions[name][action_name]
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=True)
for x in required if x != cls._id_attr]
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
required=False)
for x in optional if x != cls._id_attr]


def extend_parser(parser):
subparsers = parser.add_subparsers(title='object', dest='what',
help="Object to manipulate.")
subparsers.required = True

# populate argparse for all Gitlab Object
classes = []
for cls in gitlab.v4.objects.__dict__.values():
try:
if gitlab.base.RESTManager in inspect.getmro(cls):
if cls._obj_cls is not None:
classes.append(cls._obj_cls)
except AttributeError:
pass
classes.sort(key=operator.attrgetter("__name__"))

for cls in classes:
arg_name = cli.cls_to_what(cls)
object_group = subparsers.add_parser(arg_name)

object_subparsers = object_group.add_subparsers(
dest='action', help="Action to execute.")
_populate_sub_parser_by_class(cls, object_subparsers)
object_subparsers.required = True

return parser


class LegacyPrinter(object):
def display(self, obj, verbose=False, padding=0):
def display_dict(d):
for k in sorted(d.keys()):
v = d[k]
if isinstance(v, dict):
print('%s%s:' % (' ' * padding, k))
new_padding = padding + 2
self.display(v, True, new_padding)
continue
print('%s%s: %s' % (' ' * padding, k, v))

if verbose:
if isinstance(obj, dict):
display_dict(obj)
return

# not a dict, we assume it's a RESTObject
id = getattr(obj, obj._id_attr)
print('%s: %s' % (obj._id_attr, id))
attrs = obj.attributes
attrs.pop(obj._id_attr)
display_dict(attrs)
print('')

else:
id = getattr(obj, obj._id_attr)
print('%s: %s' % (obj._id_attr, id))
if hasattr(obj, '_short_print_attr'):
value = getattr(obj, obj._short_print_attr)
print('%s: %s' % (obj._short_print_attr, value))


def run(gl, what, action, args, verbose):
g_cli = GitlabCLI(gl, what, action, args)
ret_val = g_cli()

printer = LegacyPrinter()

if isinstance(ret_val, list):
for o in ret_val:
if isinstance(o, gitlab.base.RESTObject):
printer.display(o, verbose)
else:
print(o)
elif isinstance(ret_val, gitlab.base.RESTObject):
printer.display(ret_val, verbose)
elif isinstance(ret_val, six.string_types):
print(ret_val)

0 comments on commit 9783207

Please sign in to comment.