8000 [v4] CLI support is back · allamand/python-gitlab@9783207 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9783207

Browse files
author
Gauvain Pocentek
committed
[v4] CLI support is back
1 parent a4f0c52 commit 9783207

File tree

4 files changed

+407
-6
lines changed

4 files changed

+407
-6
lines changed

gitlab/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,13 @@ def get_id(self):
607607
return None
608608
return getattr(self, self._id_attr)
609609

610+
@property
611+
def attributes(self):
612+
d = self.__dict__['_updated_attrs'].copy()
613+
d.update(self.__dict__['_attrs'])
614+
d.update(self.__dict__['_parent_attrs'])
615+
return d
616+
610617

611618
class RESTObjectList(object):
612619
"""Generator object representing a list of RESTObject's.

gitlab/cli.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1818

1919
from __future__ import print_function
20-
from __future__ import absolute_import
2120
import argparse
21+
import functools
2222
import importlib
2323
import re
2424
import sys
@@ -27,6 +27,36 @@
2727

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

30+
# custom_actions = {
31+
# cls: {
32+
# action: (mandatory_args, optional_args, in_obj),
33+
# },
34+
# }
35+
custom_actions = {}
36+
37+
38+
def register_custom_action(cls_name, mandatory=tuple(), optional=tuple()):
39+
def wrap(f):
40+
@functools.wraps(f)
41+
def wrapped_f(*args, **kwargs):
42+
return f(*args, **kwargs)
43+
44+
# in_obj defines whether the method belongs to the obj or the manager
45+
in_obj = True
46+
final_name = cls_name
47+
if cls_name.endswith('Manager'):
48+
final_name = cls_name.replace('Manager', '')
49+
in_obj = False
50+
if final_name not in custom_actions:
51+
custom_actions[final_name] = {}
52+
53+
action = f.__name__
54+
55+
custom_actions[final_name][action] = (mandatory, optional, in_obj)
56+
57+
return wrapped_f
58+
return wrap
59+
3060

3161
def die(msg, e=None):
3262
if e:
@@ -51,6 +81,9 @@ def _get_base_parser():
5181
parser.add_argument("-v", "--verbose", "--fancy",
5282
help="Verbose mode",
5383
action="store_true")
84+
parser.add_argument("-d", "--debug",
85+
help="Debug mode (display HTTP requests",
86+
action="store_true")
5487
parser.add_argument("-c", "--config-file", action='append',
5588
help=("Configuration file to use. Can be used "
5689
"multiple times."))
@@ -84,12 +117,13 @@ def main():
84117
config_files = args.config_file
85118
gitlab_id = args.gitlab
86119
verbose = args.verbose
120+
debug = args.debug
87121
action = args.action
88122
what = args.what
89123

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

137+
if debug:
138+
gl.enable_debug()
139+
103140
cli_module.run(gl, what, action, args, verbose)
104141

105142
sys.exit(0)
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from __future__ import print_function
20+
import inspect
21+
import operator
22+
23+
import six
24+
25+
import gitlab
26+
import gitlab.base
27+
from gitlab import cli
28+
import gitlab.v4.objects
29+
30+
31+
class GitlabCLI(object):
32+
def __init__(self, gl, what, action, args):
33+
self.cls_name = cli.what_to_cls(what)
34+
self.cls = gitlab.v4.objects.__dict__[self.cls_name]
35+
self.what = what.replace('-', '_')
36+
self.action = action.lower().replace('-', '')
37+
self.gl = gl
38+
self.args = args
39+
self.mgr_cls = getattr(gitlab.v4.objects,
40+
self.cls.__name__ + 'Manager')
41+
# We could do something smart, like splitting the manager name to find
42+
# parents, build the chain of managers to get to the final object.
43+
# Instead we do something ugly and efficient: interpolate variables in
44+
# the class _path attribute, and replace the value with the result.
45+
self.mgr_cls._path = self.mgr_cls._path % self.args
46+
self.mgr = self.mgr_cls(gl)
47+
48+
def __call__(self):
49+
method = 'do_%s' % self.action
50+
if hasattr(self, method):
51+
return getattr(self, method)()
52+
else:
53+
return self.do_custom()
54+
55+
def do_custom(self):
56+
in_obj = cli.custom_actions[self.cls_name][self.action][2]
57+
58+
# Get the object (lazy), then act
59+
if in_obj:
60+
data = {}
61+
if hasattr(self.mgr, '_from_parent_attrs'):
62+
for k in self.mgr._from_parent_attrs:
63+
data[k] = self.args[k]
64+
if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.cls):
65+
data[self.cls._id_attr] = self.args.pop(self.cls._id_attr)
66+
o = self.cls(self.mgr, data)
67+
return getattr(o, self.action)(**self.args)
68+
else:
69+
return getattr(self.mgr, self.action)(**self.args)
70+
71+
def do_create(self):
72+
try:
73+
return self.mgr.create(self.args)
74+
except Exception as e:
75+
cli.die("Impossible to create object", e)
76+
77+
def do_list(self):
78+
try:
79+
return self.mgr.list(**self.args)
80+
except Exception as e:
81+
cli.die("Impossible to list objects", e)
82+
83+
def do_get(self):
84+
id = None
85+
if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(self.cls):
86+
id = self.args.pop(self.cls._id_attr)
87+
88+
try:
89+
return self.mgr.get(id, **self.args)
90+
except Exception as e:
91+
cli.die("Impossible to get object", e)
92+
93+
def do_delete(self):
94+
id = self.args.pop(self.cls._id_attr)
95+
try:
96+
self.mgr.delete(id, **self.args)
97+
except Exception as e:
98+
cli.die("Impossible to destroy object", e)
99+
100+
def do_update(self):
101+
id = self.args.pop(self.cls._id_attr)
102+
try:
103+
return self.mgr.update(id, self.args)
104+
except Exception as e:
105+
cli.die("Impossible to update object", e)
106+
107+
108+
def _populate_sub_parser_by_class(cls, sub_parser):
109+
mgr_cls_name = cls.__name__ + 'Manager'
110+
mgr_cls = getattr(gitlab.v4.objects, mgr_cls_name)
111+
112+
for action_name in ['list', 'get', 'create', 'update', 'delete']:
113+
if not hasattr(mgr_cls, action_name):
114+
continue
115+
116+
sub_parser_action = sub_parser.add_parser(action_name)
117+
if hasattr(mgr_cls, '_from_parent_attrs'):
118+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
119+
required=True)
120+
for x in mgr_cls._from_parent_attrs]
121+
sub_parser_action.add_argument("--sudo", required=False)
122+
123+
if action_name == "list":
124+
if hasattr(mgr_cls, '_list_filters'):
125+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
126+
required=False)
127+
for x in mgr_cls._list_filters]
128+
129+
sub_parser_action.add_argument("--page", required=False)
130+
sub_parser_action.add_argument("--per-page", required=False)
131+
sub_parser_action.add_argument("--all", required=False,
132+
action='store_true')
133+
134+
if action_name == 'delete':
135+
id_attr = cls._id_attr.replace('_', '-')
136+
sub_parser_action.add_argument("--%s" % id_attr, required=True)
137+
138+
if action_name == "get":
139+
if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(cls):
140+
if cls._id_attr is not None:
141+
id_attr = cls._id_attr.replace('_', '-')
142+
sub_parser_action.add_argument("--%s" % id_attr,
143+
required=True)
144+
145+
if hasattr(mgr_cls, '_optional_get_attrs'):
146+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
147+
required=False)
148+
for x in mgr_cls._optional_get_attrs]
149+
150+
if action_name == "create":
151+
if hasattr(mgr_cls, '_create_attrs'):
152+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
153+
required=True)
154+
for x in mgr_cls._create_attrs[0] if x != cls._id_attr]
155+
156+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
157+
required=False)
158+
for x in mgr_cls._create_attrs[1] if x != cls._id_attr]
159+
160+
if action_name == "update":
161+
if cls._id_attr is not None:
162+
id_attr = cls._id_attr.replace('_', '-')
163+
sub_parser_action.add_argument("--%s" % id_attr,
164+
required=True)
165+
166+
if hasattr(mgr_cls, '_update_attrs'):
167+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
168+
required=True)
169+
for x in mgr_cls._update_attrs[0] if x != cls._id_attr]
170+
171+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
172+
required=False)
173+
for x in mgr_cls._update_attrs[1] if x != cls._id_attr]
174+
175+
if cls.__name__ in cli.custom_actions:
176+
name = cls.__name__
177+
for action_name in cli.custom_actions[name]:
178+
sub_parser_action = sub_parser.add_parser(action_name)
179+
# Get the attributes for URL/path construction
180+
if hasattr(mgr_cls, '_from_parent_attrs'):
181+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
182+
required=True)
183+
for x in mgr_cls._from_parent_attrs]
184+
sub_parser_action.add_argument("--sudo", required=False)
185+
186+
# We need to get the object somehow
187+
if gitlab.mixins.GetWithoutIdMixin not in inspect.getmro(cls):
188+
if cls._id_attr is not None:
189+
id_attr = cls._id_attr.replace('_', '-')
190+
sub_parser_action.add_argument("--%s" % id_attr,
191+
required=True)
192+
193+
required, optional, dummy = cli.custom_actions[name][action_name]
194+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
195+
required=True)
196+
for x in required if x != cls._id_attr]
197+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
198+
required=False)
199+
for x in optional if x != cls._id_attr]
200+
201+
if mgr_cls.__name__ in cli.custom_actions:
202+
name = mgr_cls.__name__
203+
for action_name in cli.custom_actions[name]:
204+
sub_parser_action = sub_parser.add_parser(action_name)
205+
if hasattr(mgr_cls, '_from_parent_attrs'):
206+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
207+
required=True)
208+
for x in mgr_cls._from_parent_attrs]
209+
sub_parser_action.add_argument("--sudo", required=False)
210+
211+
required, optional, dummy = cli.custom_actions[name][action_name]
212+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
213+
required=True)
214+
for x in required if x != cls._id_attr]
215+
[sub_parser_action.add_argument("--%s" % x.replace('_', '-'),
216+
required=False)
217+
for x in optional if x != cls._id_attr]
218+
219+
220+
def extend_parser(parser):
221+
subparsers = parser.add_subparsers(title='object', dest='what',
222+
help="Object to manipulate.")
223+
subparsers.required = True
224+
225+
# populate argparse for all Gitlab Object
226+
classes = []
227+
for cls in gitlab.v4.objects.__dict__.values():
228+
try:
229+
if gitlab.base.RESTManager in inspect.getmro(cls):
230+
if cls._obj_cls is not None:
231+
classes.append(cls._obj_cls)
232+
except AttributeError:
233+
pass
234+
classes.sort(key=operator.attrgetter("__name__"))
235+
236+
for cls in classes:
237+
arg_name = cli.cls_to_what(cls)
238+
object_group = subparsers.add_parser(arg_name)
239+
240+
object_subparsers = object_group.add_subparsers(
241+
dest='action', help="Action to execute.")
242+
_populate_sub_parser_by_class(cls, object_subparsers)
243+
object_subparsers.required = True
244+
245+
return parser
246+
247+
248+
class LegacyPrinter(object):
249+
def display(self, obj, verbose=False, padding=0):
250+
def display_dict(d):
251+
for k in sorted(d.keys()):
252+
v = d[k]
253+
if isinstance(v, dict):
254+
print('%s%s:' % (' ' * padding, k))
255+
new_padding = padding + 2
256+
self.display(v, True, new_padding)
257+
continue
258+
print('%s%s: %s' % (' ' * padding, k, v))
259+
260+
if verbose:
261+
if isinstance(obj, dict):
262+
display_dict(obj)
263+
return
264+
265+
# not a dict, we assume it's a RESTObject
266+
id = getattr(obj, obj._id_attr)
267+
print('%s: %s' % (obj._id_attr, id))
268+
attrs = obj.attributes
269+
attrs.pop(obj._id_attr)
270+
display_dict(attrs)
271+
print('')
272+
273+
else:
274+
id = getattr(obj, obj._id_attr)
275+
print('%s: %s' % (obj._id_attr, id))
276+
if hasattr(obj, '_short_print_attr'):
277+
value = getattr(obj, obj._short_print_attr)
278+
print('%s: %s' % (obj._short_print_attr, value))
279+
280+
281+
def run(gl, what, action, args, verbose):
282+
g_cli = GitlabCLI(gl, what, action, args)
283+
ret_val = g_cli()
284+
285+
printer = LegacyPrinter()
286+
287+
if isinstance(ret_val, list):
288+
for o in ret_val:
289+
if isinstance(o, gitlab.base.RESTObject):
290+
printer.display(o, verbose)
291+
else:
292+
print(o)
293+
elif isinstance(ret_val, gitlab.base.RESTObject):
294+
printer.display(ret_val, verbose)
295+
elif isinstance(ret_val, six.string_types):
296+
print(ret_val)

0 commit comments

Comments
 (0)
0