[hgext] use **only** cwclientlib (closes #293628)

thus remove any endpoint configuration in hgrc but the endopint id of the forge.

Also update to cwclientlib 0.3.0

authorDavid Douard <david.douard@logilab.fr>
changeset953e124c7b2f
branchdefault
phasepublic
hiddenno
parent revision#6cd974433769 [hgext] allow using cwclientlib's config file instead of hgrc (related to #293628)
child revision#14ef7fa10b4e [hgext] add a new "start-test" command (closes #290804)
files modified by this revision
hgext/jpl/__init__.py
hgext/jpl/jplproxy.py
hgext/jpl/review.py
hgext/jpl/tasks.py
# HG changeset patch
# User David Douard <david.douard@logilab.fr>
# Date 1434386450 -7200
# Mon Jun 15 18:40:50 2015 +0200
# Node ID 953e124c7b2f472388d324b2cdabf13ca0e110fe
# Parent 6cd974433769de24bf28cbfe3529ec7e8bee5ae5
[hgext] use **only** cwclientlib (closes #293628)

thus remove any endpoint configuration in hgrc but the endopint id of the forge.

Also update to cwclientlib 0.3.0

diff --git a/hgext/jpl/__init__.py b/hgext/jpl/__init__.py
@@ -12,37 +12,29 @@
1  mercurial changesets.
2 
3  The forge url can be permanently defined into one of the mercurial
4  configuration file::
5 
6 -  [lglb]
7 -  forge-url = https://www.cubicweb.org/
8 -  auth-mech = signedrequest
9 -  auth-token = my token
10 -  auth-secret = 0123456789abcdef
11 +  [jpl]
12 +  endpoint = https://www.cubicweb.org/
13 
14 -or for kerberos authentication::
15 +or, according cwo is the id of the endpoint in your cwclientlib_ config file::
16 
17 -  [lglb]
18 -  forge-url = https://my.intranet.com/
19 -  auth-mech = kerberos
20 +  [jpl]
21 +  endpoint = cwo 
22 
23 -Note that you need `python-requests-kerberos`_ for this later
24 -configuration to work.
25 -
26 -You may also need `python-ndg-httpsclient`_ and `python-openssl`_ if
27 +You may need `python-ndg-httpsclient`_ and `python-openssl`_ if
28  the forge application is using a SNI_ ssl configuration (ie. if you
29  get errors like::
30 
31    abort: error: hostname 'www.logilab.org' doesn't match either of
32           'demo.cubicweb.org', 'cubicweb.org'
33 
34 -.. _`python-requests-kerberos`: https://pypi.python.org/pypi/requests-kerberos
35  .. _`python-ndg-httpsclient`: https://pypi.python.org/pypi/ndg-httpsclient
36  .. _`python-openssl`:https://pypi.python.org/pypi/pyOpenSSL
37  .. _SNI: https://en.wikipedia.org/wiki/Server_Name_Indication
38 -.. _`cwclientlib: https://www.cubicweb.org/project/cwclientlib
39 +.. _cwclientlib: https://www.cubicweb.org/project/cwclientlib
40 
41  '''
42  from cStringIO import StringIO
43  from mercurial import cmdutil, scmutil, util, node, demandimport
44  from mercurial.i18n import _
@@ -52,11 +44,11 @@
45  try:
46      enabled = demandimport.isenabled()
47  except AttributeError:
48      enabled = demandimport._import is __import__
49  demandimport.disable()
50 -from .jplproxy import build_proxy, RequestError
51 +from .jplproxy import build_proxy
52  from .tasks import print_tasks
53  from .review import ask_review, show_review, sudo_make_me_a_ticket, assign
54  if enabled:
55      demandimport.enable()
56 
@@ -102,12 +94,12 @@
57        TIP hidden H,
58        NOT EXISTS(RE obsoletes TIP,
59                   P patch_revision RE),
60        T concerns PO,
61        T done_in V,
62 -      V num "%(version)s",
63 -      P  patch_ticket T
64 +      V num %(version)s,
65 +      P patch_ticket T
66  """
67 
68  TASKSRQL = """
69  DISTINCT Any RC
70  WHERE P patch_revision TIP,
@@ -126,63 +118,57 @@
71  def reviewed(repo, subset, x):
72      """
73      return changesets that are linked to reviewed patch in the jpl forge
74      """
75      mercurial.revset.getargs(x, 0, 0, _("reviewed takes no arguments"))
76 -    base_url = repo.ui.config('lglb', 'forge-url')
77 -    url = '%s/view?vid=jsonexport&rql=rql:%s' % (base_url, quote(RQL))
78 -    raw_data = urlopen(url)
79 -    data = json.load(raw_data)
80 +    with build_proxy(repo.ui) as client:
81 +        data = client.rql(RQL)
82      all = set(short for po, short, p in data)
83      return [r for r in subset if str(repo[r]) in all]
84 
85  def inversion(repo, subset, x):
86      """
87      return changesets that are linked to patches linked to tickets of given version+project
88      """
89      version = mercurial.revset.getargs(x, 1, 1, _("inversion takes one argument"))[0][1]
90 -    base_url = repo.ui.config('lglb', 'forge-url')
91 -    url = '%s/view?vid=jsonexport&rql=rql:%s' % (base_url, quote(IVRQL % {'version': version}))
92 -    raw_data = urlopen(url)
93 -    data = json.load(raw_data)
94 +    with build_proxy(repo.ui) as client:
95 +        args = {'version': version}
96 +        data = client.execute(IVRQL, args)
97      all = set(short for po, short, p in data)
98      return [r for r in subset if str(repo[r]) in all]
99 
100  def tasks_predicate(repo, subset, x=None):
101      """``tasks(*states)``
102      Changesets linked to tasks to be done.
103 
104      The optional state arguments are task states to filter
105      (default to 'todo').
106      """
107 -    base_url = repo.ui.config('lglb', 'forge-url')
108      states = None
109      if x is not None:
110          states = [val for typ, val in mercurial.revset.getlist(x)]
111      if not states:
112          states = '!= "done"'
113      elif len(states) == 1:
114          states = '"{}"'.format(states[0])
115      else:
116          states = 'IN ({})'.format(','.join('"{}"'.format(state) for state in states))
117      rql = TASKSRQL.format(states=states)
118 -    url = '%s/view?vid=jsonexport&rql=rql:%s' % (base_url, quote(rql))
119 -    raw_data = urlopen(url)
120 -    data = json.load(raw_data)
121 +    with build_proxy(repo.ui) as client:
122 +        data = client.rql(rql)
123      all = set(short[0] for short in data)
124      return [r for r in subset if str(repo[r]) in all]
125 
126  def showtasks(**args):
127      """:tasks: List of Strings. The text of the tasks and comments of a patch."""
128      output = _MockOutput()
129      with build_proxy(output, args) as client:
130          try:
131              print_tasks(client, output, iter([node.short(args['ctx'].node())]), {})
132 -        except RequestError:
133 +        except Exception:
134              return ''
135      return mercurial.templatekw.showlist('task', list(output), **args)
136 -    #return str(output).strip()
137 
138  class _MockOutput(object):
139      def __init__(self):
140          self._ios = [StringIO()]
141      def write(self, msg, label=None):
@@ -192,22 +178,18 @@
142      def __iter__(self):
143          for io in self._ios:
144              yield io.getvalue()
145 
146  def extsetup(ui):
147 -    if ui.config('lglb', 'forge-url'):
148 +    if ui.config('jpl', 'endpoint'):
149          mercurial.revset.symbols['reviewed'] = reviewed
150          mercurial.revset.symbols['tasks'] = tasks_predicate
151          mercurial.revset.symbols['inversion'] = inversion
152          mercurial.templatekw.keywords['tasks'] = showtasks
153 
154  cnxopts  = [
155 -    ('U', 'forge-url', '', _('base url of the forge (jpl) server'), _('URL')),
156 -    ('S', 'no-verify-ssl', None, _('do NOT verify server SSL certificate')),
157 -    ('Y', 'auth-mech', '', _('authentication mechanism used to connect to the forge'), _('MECH')),
158 -    ('t', 'auth-token', '', _('authentication token (when using signed request)'), _('TOKEN')),
159 -    ('s', 'auth-secret', '', _('authentication secret (when using signed request)'), ('SECRET')),
160 +    ('U', 'endpoint', '', _('endpoint (ID or URL) of the configured cwclientlib forge (jpl) server'), _('ENDPOINT')),
161      ]
162 
163  @command('^tasks', [
164      ('r', 'rev', [], _('tasks for the given revision(s)'), _('REV')),
165      ('a', 'all', False, _('also display done tasks')),
@@ -217,19 +199,21 @@
166      """show tasks related to the given revision.
167 
168      By default, the revision used is the parent of the working
169      directory: use -r/--rev to specify a different revision.
170 
171 -    By default, the forge url used is https://www.cubicweb.org/: use
172 -    -U/--forge-url to specify a different url. The forge url can be
173 -    permanently defined into one of the mercurial configuration file::
174 +    By default, the forge url used is https://www.cubicweb.org/. Use
175 +    -U/--endpoint to specify a different cwclientlib endpoint. The
176 +    endpoint id of the forge can be permanently defined into one of
177 +    the mercurial configuration file::
178 
179 -    [lglb]
180 -    forge-url = https://www.cubicweb.org/
181 +    [jpl]
182 +    endpoint = https://www.cubicweb.org/
183 
184      By default, done tasks are not displayed: use -a/--all to not filter
185      tasks and display all.
186 +
187      """
188      changesets += tuple(opts.get('rev', []))
189      if not changesets:
190          changesets = ('.')
191      revs = scmutil.revrange(repo, changesets)
@@ -243,11 +227,11 @@
192          ctxhexs = list((node.short(repo.lookup(lrev)) for lrev in precs))
193          showall = opts.get('all', None)
194          with build_proxy(ui, opts) as client:
195              try:
196                  print_tasks(client, ui, ctxhexs, showall=showall)
197 -            except RequestError, e:
198 +            except Exception as e:
199                  ui.write('no patch or no tasks for %s\n' % node.short(repo.lookup(rev)))
200 
201 
202  @command('^ask-review', [
203      ('r', 'rev', [], _('ask review for the given revision(s)'), _('REV')),
diff --git a/hgext/jpl/jplproxy.py b/hgext/jpl/jplproxy.py
@@ -7,13 +7,12 @@
204  from contextlib import contextmanager
205  from mercurial import util
206  from mercurial.i18n import _
207  from requests import ConnectionError, HTTPError
208 
209 -import itertools
210 +from cwclientlib import cwproxy, cwproxy_for
211 
212 -from cwclientlib import cwproxy, cwproxy_for
213 
214  def wraprql(meth):
215      def wrapper(*args, **kwargs):
216          reply = meth(*args, **kwargs)
217          try:
@@ -25,64 +24,44 @@
218              return None
219          except HTTPError as exc:
220              return '\n'.join(("%s" % exc, reply.json()['reason']))
221      return wrapper
222 
223 -class RequestError(IOError):
224 -    """Exception raised when the request fails."""
225 +
226 +class JplProxy(cwproxy.CWProxy):
227 +    rql = wraprql(cwproxy.CWProxy.rql)
228 +    rqlio = wraprql(cwproxy.CWProxy.rqlio)
229 
230 -def getlglbopt(name, ui, opts, default=None, isbool=False):
231 +    def execute(self, rql, args=None):
232 +        # reimplemented since rqlio is wrapped
233 +        result = self.rqlio([(rql, args)])
234 +        if isinstance(result, list):
235 +            result = result[0]
236 +        return result
237 +
238 +
239 +def getcwcliopt(name, ui, opts, default=None, isbool=False):
240      value = default
241 -    if getattr(ui, 'config', None) and ui.config('lglb', name):
242 -        value = ui.config('lglb', name)
243 +    if getattr(ui, 'config', None) and ui.config('jpl', name):
244 +        value = ui.config('jpl', name)
245      name = name.replace('-', '_')
246 -    if opts.get(name):
247 +    if opts and opts.get(name):
248          value = opts[name]
249      if isbool and value not in (None, True, False):
250          value = value.lower() in ('t','true','1','y','yes')
251      return value
252 
253 +
254  @contextmanager
255 -def build_proxy(ui, opts):
256 +def build_proxy(ui, opts=None):
257      """Build a cwproxy"""
258 -
259      try:
260 -        base_url = getlglbopt('forge-url', ui, opts, default=URL)
261 -        verify = not getlglbopt('no-verify-ssl', ui, opts, isbool=True)
262 -        mech = getlglbopt('auth-mech', ui, opts)
263 -        auth = None
264 -
265 -        if base_url.startswith('http'):
266 -            # legacy, all config in hgrc
267 -            if mech and mech not in ('signedrequest', 'kerberos'):
268 -                raise util.Abort(_('unknown authentication mechanisme specified with --auth-mech'))
269 -
270 -            if mech == 'signedrequest':
271 -                token = getlglbopt('auth-token', ui, opts)
272 -                secret = getlglbopt('auth-secret', ui, opts)
273 -                if not token or not secret:
274 -                    raise util.Abort(_('you must provide your authentication token and secret'))
275 -
276 -                auth = cwproxy.SignedRequestAuth(token, secret)
277 -            if mech == 'kerberos':
278 -                from requests_kerberos import HTTPKerberosAuth, OPTIONAL
279 -                auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL)
280 -
281 -            proxy = cwproxy.CWProxy(base_url, auth=auth, verify=verify)
282 -        else:
283 -            # use cwclientlib config file
284 -            proxy = cwproxy_for(base_url)
285 -
286 -        proxy.rql = wraprql(proxy.rql)
287 -        proxy.rqlio = wraprql(proxy.rqlio)
288 -        yield proxy
289 -
290 +        endpoint = getcwcliopt('endpoint', ui, opts, default=URL)
291 +        yield cwproxy_for(endpoint, proxycls=JplProxy)
292      except ConnectionError as exc:
293          if ui.tracebackflag:
294              raise
295          try:
296              msg = exc[0].reason
297          except (AttributeError, IndexError):
298              msg = str(exc)
299          ui.warn(_('abort: error: %s\n') % msg)
300 -
301 -
diff --git a/hgext/jpl/review.py b/hgext/jpl/review.py
@@ -4,11 +4,11 @@
302  import itertools
303  import sys
304  enc = sys.stdout.encoding or 'ascii'
305 
306  from cwclientlib import builders
307 -from .jplproxy import build_proxy, RequestError
308 +from .jplproxy import build_proxy
309 
310  def ask_review(client, revs):
311      eids = client.rql(
312          '''Any P WHERE P patch_revision R, R changeset IN ({revs}),
313                         P in_state S, S name 'in-progress'
diff --git a/hgext/jpl/tasks.py b/hgext/jpl/tasks.py
@@ -3,11 +3,11 @@
314 
315  import itertools
316  import sys
317  enc = sys.stdout.encoding or 'ascii'
318 
319 -from .jplproxy import build_proxy, RequestError
320 +from .jplproxy import build_proxy
321 
322  INDENT = '  '
323 
324  PATCH_RQL = """
325  rql:
@@ -54,12 +54,12 @@
326      ui.write(msg.encode(enc, 'replace'), label='jpl.tasks.comment')
327      for c in comment['comments']:
328          print_comment(ui, c, level=level + 1)
329 
330 
331 -def print_task(ui, reveid, teid, ttitle, tstate, tdesc, baseurl):
332 -    msg = u'{indent}[{state}] {title} ({baseurl}/{eid})\n'.format(indent=INDENT, baseurl=baseurl, eid=teid,
333 +def print_task(ui, reveid, ttitle, tstate, tdesc, url):
334 +    msg = u'{indent}[{state}] {title} ({url})\n'.format(indent=INDENT, url=url,
335                                                           state=tstate.upper(), title=ttitle)
336      ui.write(msg.encode(enc, 'replace'), label='jpl.tasks.task.{state}'.format(state=tstate))
337      if tdesc:
338          desc = tdesc.strip().replace('\n', '\n' + INDENT)
339          msg = u'{indent}{content}\n'.format(indent=INDENT, content=desc)
@@ -80,22 +80,22 @@
340          rql = PATCH_RQL.format(unions='UNION'.join(UNIONS[:2]))
341          taskstate = TASKNOTDONE_RQL
342 
343      patchesdata = client.rql(rql.format(revs=revs, taskstate=taskstate), vid='jsonexport')
344      if not patchesdata:
345 -        raise RequestError("no tasks found for revisions: %r" % ','.join(revs))
346 +        raise ValueError("no tasks found for revisions: %r" % ','.join(revs))
347 
348      patchesdata = itertools.groupby(patchesdata, lambda x:x[:3])
349      for (peid, pname, pstate), patchdata in patchesdata:
350 -        msg = '{name} {url}/{eid:d} ({state})\n\n'.format(url=client.base_url, eid=peid, state=pstate, name=pname)
351 +        msg = '{name} {url} ({state})\n\n'.format(url=client.build_url(str(peid)), state=pstate, name=pname)
352          ui.write(msg, label='jpl.tasks.patch')
353          teids = set()
354          for peid, pname, pstate, reveid, teid, ttitle, tdesc, tstate in patchdata:
355              if teid is None or teid in teids:
356                  continue
357              teids.add(teid)
358 -            print_task(ui, reveid, teid, ttitle, tstate, tdesc, client.base_url)
359 +            print_task(ui, reveid, ttitle, tstate, tdesc, client.build_url(str(teid)))
360 
361      # tasks = client.rql(rql.format(**patch), vid='ejsonexport')
362      # comments = []
363 
364      # if not tasks:
@@ -122,12 +122,12 @@
365      epilog=('The `colorama <https://pypi.python.org/pypi/colorama>`_ module is '
366              'required to enable colored output.')
367      parser = argparse.ArgumentParser(epilog=epilog)
368      parser.add_argument('revs', default=[], metavar='REVS', nargs='+',
369                          help='tasks for the given revision (short hex)', ),
370 -    parser.add_argument('-U', '--forge-url', default=URL, metavar='URL',
371 -                        help='base url of the forge (jpl) server [%s]' % URL)
372 +    parser.add_argument('-U', '--endpoint', default=URL, metavar='URL',
373 +                        help='cwclientlib endpoint ID of the forge (jpl) server [%s]' % URL)
374      parser.add_argument('-c', '--color', default='auto', metavar='WHEN',
375                          choices=('auto', 'never', 'always'),
376                          help='display data with color [auto]')
377      parser.add_argument('-a', '--all', default=False, action='store_true',
378                          help='display data with color [auto]')