##// END OF EJS Templates
Formatting.
Marcin Kasperski -
r151:fb526db1 default
parent child Browse files
Show More
@@ -1,563 +1,563 b''
1 1 # -*- coding: utf-8 -*-
2 2 #
3 3 # mercurial_keyring: save passwords in password database
4 4 #
5 5 # Copyright (c) 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 6 # All rights reserved.
7 7 #
8 8 # Redistribution and use in source and binary forms, with or without
9 9 # modification, are permitted provided that the following conditions
10 10 # are met:
11 11 # 1. Redistributions of source code must retain the above copyright
12 12 # notice, this list of conditions and the following disclaimer.
13 13 # 2. Redistributions in binary form must reproduce the above copyright
14 14 # notice, this list of conditions and the following disclaimer in the
15 15 # documentation and/or other materials provided with the distribution.
16 16 # 3. The name of the author may not be used to endorse or promote products
17 17 # derived from this software without specific prior written permission.
18 18 #
19 19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 29 #
30 30 # See README.txt for more details.
31 31
32 32 ''' securely save HTTP and SMTP authentication details
33 33 mercurial_keyring is a Mercurial extension used to securely save
34 34 HTTP and SMTP authentication passwords in password databases (Gnome
35 35 Keyring, KDE KWallet, OSXKeyChain, specific solutions for Win32 and
36 36 command line). This extension uses and wraps services of the keyring
37 37 library.
38 38 '''
39 39
40 40 from mercurial import util, sslutil
41 41 from mercurial.i18n import _
42 42 try:
43 43 from mercurial.url import passwordmgr
44 44 except:
45 45 from mercurial.httprepo import passwordmgr
46 46 from mercurial import mail
47 47 from mercurial.mail import SMTPS, STARTTLS
48 48 from mercurial import encoding
49 49 from urlparse import urlparse
50 50 import urllib2
51 51 import smtplib
52 52 import socket
53 53 import os
54 54 import sys
55 55
56 56
57 57 def import_keyring():
58 58 """
59 59 Importing keyring happens to be costly if wallet is slow, so we delay it
60 60 until really needed.
61 61 """
62 62 if 'keyring' in sys.modules:
63 63 return
64 64 # mercurial.demandimport incompatibility workaround.
65 65 # various keyring backends fail as they can't properly import helper
66 66 # modules (as demandimport modifies python import behaviour).
67 67 # If you get import errors with demandimport in backtrace, try
68 68 # guessing what to block and extending the list below.
69 69 from mercurial import demandimport
70 70 for blocked_module in [
71 71 "gobject._gobject",
72 72 "configparser",
73 73 "json",
74 74 "abc",
75 75 "io",
76 76 "keyring",
77 77 "gdata.docs.service",
78 78 "gdata.service",
79 79 "types",
80 80 "atom.http",
81 81 "atom.http_interface",
82 82 "atom.service",
83 83 "atom.token_store",
84 84 "ctypes",
85 85 "secretstorage.exceptions",
86 86 "fs.opener",
87 87 ]:
88 88 if blocked_module not in demandimport.ignore:
89 89 demandimport.ignore.append(blocked_module)
90 90 try:
91 91 is_demandimport_enabled = demandimport.isenabled
92 92 except AttributeError:
93 93 # Mercurial < 2.9.1
94 94 try:
95 95 orig_demandimport = demandimport.demandimport
96 96 except AttributeError:
97 97 orig_demandimport = demandimport._demandimport
98 98 def is_demandimport_enabled():
99 99 return __import__ == orig_demandimport
100 100
101 101 # Shut up warning about uninitialized logging for new keyring versions
102 102 import logging
103 103 logging.getLogger("keyring").addHandler(logging.NullHandler())
104 104
105 105 # Temporarily disable demandimport to make the need of extending
106 106 # the list above less likely.
107 107 if is_demandimport_enabled():
108 108 demandimport.disable()
109 109 try:
110 110 import keyring
111 111 finally:
112 112 demandimport.enable()
113 113 else:
114 114 import keyring
115 115
116 116 KEYRING_SERVICE = "Mercurial"
117 117
118 118 ############################################################
119 119
120 def monkeypatch_method(cls,fname=None):
120 def monkeypatch_method(cls, fname=None):
121 121 def decorator(func):
122 122 local_fname = fname
123 123 if local_fname is None:
124 124 local_fname = func.__name__
125 125 setattr(func, "orig", getattr(cls, local_fname, None))
126 126 setattr(cls, local_fname, func)
127 127 return func
128 128 return decorator
129 129
130 130 ############################################################
131 131
132 132 class PasswordStore(object):
133 133 """
134 134 Helper object handling keyring usage (password save&restore,
135 135 the way passwords are keyed in the keyring).
136 136 """
137 137 def __init__(self):
138 138 self.cache = dict()
139 139 def get_http_password(self, url, username):
140 140 return self._read_password_from_keyring(
141 141 self._format_http_key(url, username))
142 142 def set_http_password(self, url, username, password):
143 143 self._save_password_to_keyring(
144 144 self._format_http_key(url, username),
145 145 password)
146 146 def clear_http_password(self, url, username):
147 147 self.set_http_password(url, username, "")
148 148 def _format_http_key(self, url, username):
149 149 return "%s@@%s" % (username, url)
150 150 def get_smtp_password(self, machine, port, username):
151 151 return self._read_password_from_keyring(
152 152 self._format_smtp_key(machine, port, username))
153 153 def set_smtp_password(self, machine, port, username, password):
154 154 self._save_password_to_keyring(
155 155 self._format_smtp_key(machine, port, username),
156 156 password)
157 157 def clear_smtp_password(self, machine, port, username):
158 158 self.set_smtp_password(machine, port, username, "")
159 159 def _format_smtp_key(self, machine, port, username):
160 160 return "%s@@%s:%s" % (username, machine, str(port))
161 161 def _read_password_from_keyring(self, pwdkey):
162 162 import_keyring()
163 163 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
164 164 # Reverse recoding from next routine
165 165 if isinstance(password, unicode):
166 166 return encoding.tolocal(password.encode('utf-8'))
167 167 return password
168 168 def _save_password_to_keyring(self, pwdkey, password):
169 169 import_keyring()
170 170 # keyring in general expects unicode. Mercurial provides "local" encoding. See #33
171 171 password = encoding.fromlocal(password).decode('utf-8')
172 172 keyring.set_password(KEYRING_SERVICE,
173 173 pwdkey,
174 174 password)
175 175
176 176 password_store = PasswordStore()
177 177
178 178 ############################################################
179 179
180 180 def _debug(ui, msg):
181 181 ui.debug("[HgKeyring] " + msg + "\n")
182 182
183 183 def _debug_reply(ui, msg, url, user, pwd):
184 184 _debug(ui, "%s. Url: %s, user: %s, passwd: %s" % (
185 185 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
186 186
187 187
188 188 ############################################################
189 189
190 190 class HTTPPasswordHandler(object):
191 191 """
192 192 Actual implementation of password handling (user prompting,
193 193 configuration file searching, keyring save&restore).
194 194
195 195 Object of this class is bound as passwordmgr attribute.
196 196 """
197 197 def __init__(self):
198 198 self.pwd_cache = {}
199 199 self.last_reply = None
200 200
201 201 def find_auth(self, pwmgr, realm, authuri, req):
202 202 """
203 203 Actual implementation of find_user_password - different
204 204 ways of obtaining the username and password.
205 205 """
206 206 ui = pwmgr.ui
207 207
208 208 # If we are called again just after identical previous
209 209 # request, then the previously returned auth must have been
210 210 # wrong. So we note this to force password prompt (and avoid
211 211 # reusing bad password indifinitely).
212 212 after_bad_auth = (self.last_reply \
213 213 and (self.last_reply['realm'] == realm) \
214 214 and (self.last_reply['authuri'] == authuri) \
215 215 and (self.last_reply['req'] == req))
216 216 if after_bad_auth:
217 217 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
218 218
219 219 # Strip arguments to get actual remote repository url.
220 220 base_url = self.canonical_url(authuri)
221 221
222 222 # Extracting possible username (or password)
223 223 # stored directly in repository url
224 224 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
225 225 pwmgr, realm, authuri)
226 226 if user and pwd:
227 227 _debug_reply(ui, _("Auth data found in repository URL"),
228 228 base_url, user, pwd)
229 229 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
230 230 return user, pwd
231 231
232 232 # Loading .hg/hgrc [auth] section contents. If prefix is given,
233 233 # it will be used as a key to lookup password in the keyring.
234 234 auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url, user)
235 235 if prefix_url:
236 236 keyring_url = prefix_url
237 237 else:
238 238 keyring_url = base_url
239 239 _debug(ui, _("Keyring URL: %s") % keyring_url)
240 240
241 241 # Checking the memory cache (there may be many http calls per command)
242 242 cache_key = (realm, keyring_url)
243 243 if not after_bad_auth:
244 244 cached_auth = self.pwd_cache.get(cache_key)
245 245 if cached_auth:
246 246 user, pwd = cached_auth
247 247 _debug_reply(ui, _("Cached auth data found"),
248 248 base_url, user, pwd)
249 249 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
250 250 return user, pwd
251 251
252 252 if auth_user:
253 253 if user and (user != auth_user):
254 254 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, auth_user)))
255 255 user = auth_user
256 256 if pwd:
257 257 self.pwd_cache[cache_key] = user, pwd
258 258 _debug_reply(ui, _("Auth data set in .hg/hgrc"),
259 259 base_url, user, pwd)
260 260 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
261 261 return user, pwd
262 262 else:
263 263 _debug(ui, _("Username found in .hg/hgrc: %s") % user)
264 264
265 265 # Loading password from keyring.
266 266 # Only if username is known (so we know the key) and we are
267 267 # not after failure (so we don't reuse the bad password).
268 268 if user and not after_bad_auth:
269 269 _debug(ui, _("Looking for password for user %s and url %s") % (user, keyring_url))
270 270 pwd = password_store.get_http_password(keyring_url, user)
271 271 if pwd:
272 272 self.pwd_cache[cache_key] = user, pwd
273 273 _debug_reply(ui, _("Keyring password found"),
274 274 base_url, user, pwd)
275 275 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
276 276 return user, pwd
277 277 else:
278 278 _debug(ui, _("Password not present in the keyring"))
279 279
280 280 # Is the username permanently set?
281 281 fixed_user = (user and True or False)
282 282
283 283 # Last resort: interactive prompt
284 284 if not ui.interactive():
285 285 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
286 286
287 287 if not fixed_user:
288 288 ui.status(_("Username not specified in .hg/hgrc. Keyring will not be used.\n"))
289 289
290 290 ui.write(_("http authorization required\n"))
291 291 ui.status(_("realm: %s\n") % realm)
292 292 if fixed_user:
293 293 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
294 294 else:
295 295 user = ui.prompt(_("user:"), default=None)
296 296 pwd = ui.getpass(_("password: "))
297 297
298 298 if fixed_user:
299 299 # Saving password to the keyring.
300 300 # It is done only if username is permanently set.
301 301 # Otherwise we won't be able to find the password so it
302 302 # does not make much sense to preserve it
303 303 _debug(ui, _("Saving password for %s to keyring") % user)
304 304 password_store.set_http_password(keyring_url, user, pwd)
305 305
306 306 # Saving password to the memory cache
307 307 self.pwd_cache[cache_key] = user, pwd
308 308
309 309 _debug_reply(ui, _("Manually entered password"),
310 310 base_url, user, pwd)
311 311 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
312 312 return user, pwd
313 313
314 314 def load_hgrc_auth(self, ui, base_url, user):
315 315 """
316 316 Loading [auth] section contents from local .hgrc
317 317
318 318 Returns (username, password, prefix) tuple (every
319 319 element can be None)
320 320 """
321 321 # Theoretically 3 lines below should do:
322 322
323 323 #auth_token = self.readauthtoken(base_url)
324 324 #if auth_token:
325 325 # user, pwd = auth.get('username'), auth.get('password')
326 326
327 327 # Unfortunately they do not work, readauthtoken always return
328 328 # None. Why? Because ui (self.ui of passwordmgr) describes the
329 329 # *remote* repository, so does *not* contain any option from
330 330 # local .hg/hgrc.
331 331
332 332 # TODO: mercurial 1.4.2 is claimed to resolve this problem
333 333 # (thanks to: http://hg.xavamedia.nl/mercurial/crew/rev/fb45c1e4396f)
334 334 # so since this version workaround implemented below should
335 335 # not be necessary. As it will take some time until people
336 336 # migrate to >= 1.4.2, it would be best to implement
337 337 # workaround conditionally.
338 338
339 339 # Workaround: we recreate the repository object
340 340 repo_root = ui.config("bundle", "mainreporoot")
341 341
342 342 from mercurial.ui import ui as _ui
343 343 local_ui = _ui(ui)
344 344 if repo_root:
345 345 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
346 346 try:
347 347 local_passwordmgr = passwordmgr(local_ui)
348 348 auth_token = local_passwordmgr.readauthtoken(base_url)
349 349 except AttributeError:
350 350 try:
351 351 # hg 1.8
352 352 import mercurial.url
353 353 readauthforuri = mercurial.url.readauthforuri
354 354 except (ImportError, AttributeError):
355 355 # hg 1.9
356 356 import mercurial.httpconnection
357 357 readauthforuri = mercurial.httpconnection.readauthforuri
358 358 if readauthforuri.func_code.co_argcount == 3:
359 359 # Since hg.0593e8f81c71
360 360 res = readauthforuri(local_ui, base_url, user)
361 361 else:
362 362 res = readauthforuri(local_ui, base_url)
363 363 if res:
364 364 group, auth_token = res
365 365 else:
366 366 auth_token = None
367 367 if auth_token:
368 368 username = auth_token.get('username')
369 369 password = auth_token.get('password')
370 370 prefix = auth_token.get('prefix')
371 371 shortest_url = self.shortest_url(base_url, prefix)
372 372 return username, password, shortest_url
373 373
374 374 return None, None, None
375 375
376 376 def shortest_url(self, base_url, prefix):
377 377 if not prefix or prefix == '*':
378 378 return base_url
379 379 scheme, hostpath = base_url.split('://', 1)
380 380 p = prefix.split('://', 1)
381 381 if len(p) > 1:
382 382 prefix_host_path = p[1]
383 383 else:
384 384 prefix_host_path = prefix
385 385 shortest_url = scheme + '://' + prefix_host_path
386 386 return shortest_url
387 387
388 388 def canonical_url(self, authuri):
389 389 """
390 390 Strips query params from url. Used to convert urls like
391 391 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
392 392 to
393 393 https://repo.machine.com/repos/apps/module
394 394 """
395 395 parsed_url = urlparse(authuri)
396 396 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
397 397 parsed_url.path)
398 398
399 399 ############################################################
400 400
401 401 @monkeypatch_method(passwordmgr)
402 402 def find_user_password(self, realm, authuri):
403 403 """
404 404 keyring-based implementation of username/password query
405 405 for HTTP(S) connections
406 406
407 407 Passwords are saved in gnome keyring, OSX/Chain or other platform
408 408 specific storage and keyed by the repository url
409 409 """
410 410 # Extend object attributes
411 411 if not hasattr(self, '_pwd_handler'):
412 412 self._pwd_handler = HTTPPasswordHandler()
413 413
414 414 if hasattr(self, '_http_req'):
415 415 req = self._http_req
416 416 else:
417 417 req = None
418 418
419 419 return self._pwd_handler.find_auth(self, realm, authuri, req)
420 420
421 421 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
422 422 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
423 423 self.passwd._http_req = req
424 424 try:
425 425 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
426 426 finally:
427 427 self.passwd._http_req = None
428 428
429 429 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
430 430 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
431 431 self.passwd._http_req = req
432 432 try:
433 433 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
434 434 finally:
435 435 self.passwd._http_req = None
436 436
437 437 ############################################################
438 438
439 439 def try_smtp_login(ui, smtp_obj, username, password):
440 440 """
441 441 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
442 442 password.
443 443
444 444 Returns:
445 445 - True if login succeeded
446 446 - False if login failed due to the wrong credentials
447 447
448 448 Throws Abort exception if login failed for any other reason.
449 449
450 450 Immediately returns False if password is empty
451 451 """
452 452 if not password:
453 453 return False
454 454 try:
455 455 ui.note(_('(authenticating to mail server as %s)\n') %
456 456 (username))
457 457 smtp_obj.login(username, password)
458 458 return True
459 459 except smtplib.SMTPException, inst:
460 460 if inst.smtp_code == 535:
461 461 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
462 462 return False
463 463 else:
464 464 raise util.Abort(inst)
465 465
466 466 def keyring_supported_smtp(ui, username):
467 467 """
468 468 keyring-integrated replacement for mercurial.mail._smtp
469 469 Used only when configuration file contains username, but
470 470 does not contain the password.
471 471
472 472 Most of the routine below is copied as-is from
473 473 mercurial.mail._smtp. The only changed part is
474 474 marked with #>>>>> and #<<<<< markers
475 475 """
476 476 local_hostname = ui.config('smtp', 'local_hostname')
477 477 tls = ui.config('smtp', 'tls', 'none')
478 478 # backward compatible: when tls = true, we use starttls.
479 479 starttls = tls == 'starttls' or util.parsebool(tls)
480 480 smtps = tls == 'smtps'
481 481 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
482 482 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
483 483 mailhost = ui.config('smtp', 'host')
484 484 if not mailhost:
485 485 raise util.Abort(_('smtp.host not configured - cannot send mail'))
486 486 verifycert = ui.config('smtp', 'verifycert', 'strict')
487 487 if verifycert not in ['strict', 'loose']:
488 488 if util.parsebool(verifycert) is not False:
489 489 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
490 490 % (verifycert))
491 491 verifycert = False
492 492 if (starttls or smtps) and verifycert:
493 493 sslkwargs = sslutil.sslkwargs(ui, mailhost)
494 494 else:
495 495 sslkwargs = {}
496 496 if smtps:
497 497 ui.note(_('(using smtps)\n'))
498 498 s = SMTPS(sslkwargs, local_hostname=local_hostname)
499 499 elif starttls:
500 500 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
501 501 else:
502 502 s = smtplib.SMTP(local_hostname=local_hostname)
503 503 if smtps:
504 504 defaultport = 465
505 505 else:
506 506 defaultport = 25
507 507 mailport = util.getport(ui.config('smtp', 'port', defaultport))
508 508 ui.note(_('sending mail: smtp host %s, port %s\n') %
509 509 (mailhost, mailport))
510 510 s.connect(host=mailhost, port=mailport)
511 511 if starttls:
512 512 ui.note(_('(using starttls)\n'))
513 513 s.ehlo()
514 514 s.starttls()
515 515 s.ehlo()
516 516 if (starttls or smtps) and verifycert:
517 517 ui.note(_('(verifying remote certificate)\n'))
518 518 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
519 519
520 520 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
521 521 stored = password = password_store.get_smtp_password(
522 522 mailhost, mailport, username)
523 523 # No need to check whether password was found as try_smtp_login
524 524 # just returns False if it is absent.
525 525 while not try_smtp_login(ui, s, username, password):
526 526 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
527 527
528 528 if stored != password:
529 529 password_store.set_smtp_password(
530 530 mailhost, mailport, username, password)
531 531 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
532 532
533 533 def send(sender, recipients, msg):
534 534 try:
535 535 return s.sendmail(sender, recipients, msg)
536 536 except smtplib.SMTPRecipientsRefused, inst:
537 537 recipients = [r[1] for r in inst.recipients.values()]
538 538 raise util.Abort('\n' + '\n'.join(recipients))
539 539 except smtplib.SMTPException, inst:
540 540 raise util.Abort(inst)
541 541
542 542 return send
543 543
544 544 ############################################################
545 545
546 546 orig_smtp = mail._smtp
547 547
548 548 @monkeypatch_method(mail)
549 549 def _smtp(ui):
550 550 """
551 551 build an smtp connection and return a function to send email
552 552
553 553 This is the monkeypatched version of _smtp(ui) function from
554 554 mercurial/mail.py. It calls the original unless username
555 555 without password is given in the configuration.
556 556 """
557 557 username = ui.config('smtp', 'username')
558 558 password = ui.config('smtp', 'password')
559 559
560 560 if username and not password:
561 561 return keyring_supported_smtp(ui, username)
562 562 else:
563 563 return orig_smtp(ui)
@@ -1,37 +1,37 b''
1 1
2 version = '0.6.7'
2 version = '0.7.0'
3 3
4 4 try:
5 from setuptools import setup, find_packages
5 from setuptools import setup, find_packages
6 6 except ImportError:
7 from ez_setup import use_setuptools
8 use_setuptools()
9 from setuptools import setup, find_packages
7 from ez_setup import use_setuptools
8 use_setuptools()
9 from setuptools import setup, find_packages
10 10
11 11 long_description = open("README.txt").read()
12 12
13 13 setup(
14 name = "mercurial_keyring",
15 version = version,
16 author = 'Marcin Kasperski',
17 author_email = 'Marcin.Kasperski@mekk.waw.pl',
18 url = 'http://bitbucket.org/Mekk/mercurial_keyring',
19 description = 'Mercurial Keyring Extension',
20 long_description = long_description,
21 license = 'BSD',
22 py_modules = ['mercurial_keyring'],
23 keywords = "mercurial hg keyring password",
24 classifiers = [
25 'Development Status :: 4 - Beta',
26 'Environment :: Console',
27 'Intended Audience :: Developers',
28 'License :: DFSG approved',
29 'License :: OSI Approved :: BSD License',
30 'Operating System :: OS Independent',
31 'Topic :: Software Development :: Libraries',
32 'Topic :: Software Development :: Libraries :: Python Modules',
33 'Topic :: Software Development :: Version Control'
34 ],
35 install_requires = ['keyring>=0.3'],
36 zip_safe = True,
37 )
14 name = "mercurial_keyring",
15 version = version,
16 author = 'Marcin Kasperski',
17 author_email = 'Marcin.Kasperski@mekk.waw.pl',
18 url = 'http://bitbucket.org/Mekk/mercurial_keyring',
19 description = 'Mercurial Keyring Extension',
20 long_description = long_description,
21 license = 'BSD',
22 py_modules = ['mercurial_keyring'],
23 keywords = "mercurial hg keyring password",
24 classifiers = [
25 'Development Status :: 4 - Beta',
26 'Environment :: Console',
27 'Intended Audience :: Developers',
28 'License :: DFSG approved',
29 'License :: OSI Approved :: BSD License',
30 'Operating System :: OS Independent',
31 'Topic :: Software Development :: Libraries',
32 'Topic :: Software Development :: Libraries :: Python Modules',
33 'Topic :: Software Development :: Version Control'
34 ],
35 install_requires = ['keyring>=0.3'],
36 zip_safe = True,
37 )
General Comments 0
You need to be logged in to leave comments. Login now