##// END OF EJS Templates
utils: catch error: [Errno 98] Address already in use errors
super-admin -
r4882:f20a077b default
parent child Browse files
Show More
@@ -1,1207 +1,1212 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Some simple helper functions
24 24 """
25 25
26 26 import collections
27 27 import datetime
28 28 import dateutil.relativedelta
29 29 import hashlib
30 30 import logging
31 31 import re
32 32 import sys
33 33 import time
34 34 import urllib
35 35 import urlobject
36 36 import uuid
37 37 import getpass
38 38 import socket
39 import errno
39 40 import random
40 41 from functools import update_wrapper, partial, wraps
41 42
42 43 import pygments.lexers
43 44 import sqlalchemy
44 45 import sqlalchemy.engine.url
45 46 import sqlalchemy.exc
46 47 import sqlalchemy.sql
47 48 import webob
48 49 import pyramid.threadlocal
49 50 from pyramid import compat
50 51 from pyramid.settings import asbool
51 52
52 53 import rhodecode
53 54 from rhodecode.translation import _, _pluralize
54 55
55 56
56 57 def md5(s):
57 58 return hashlib.md5(s).hexdigest()
58 59
59 60
60 61 def md5_safe(s):
61 62 return md5(safe_str(s))
62 63
63 64
64 65 def sha1(s):
65 66 return hashlib.sha1(s).hexdigest()
66 67
67 68
68 69 def sha1_safe(s):
69 70 return sha1(safe_str(s))
70 71
71 72
72 73 def __get_lem(extra_mapping=None):
73 74 """
74 75 Get language extension map based on what's inside pygments lexers
75 76 """
76 77 d = collections.defaultdict(lambda: [])
77 78
78 79 def __clean(s):
79 80 s = s.lstrip('*')
80 81 s = s.lstrip('.')
81 82
82 83 if s.find('[') != -1:
83 84 exts = []
84 85 start, stop = s.find('['), s.find(']')
85 86
86 87 for suffix in s[start + 1:stop]:
87 88 exts.append(s[:s.find('[')] + suffix)
88 89 return [e.lower() for e in exts]
89 90 else:
90 91 return [s.lower()]
91 92
92 93 for lx, t in sorted(pygments.lexers.LEXERS.items()):
93 94 m = map(__clean, t[-2])
94 95 if m:
95 96 m = reduce(lambda x, y: x + y, m)
96 97 for ext in m:
97 98 desc = lx.replace('Lexer', '')
98 99 d[ext].append(desc)
99 100
100 101 data = dict(d)
101 102
102 103 extra_mapping = extra_mapping or {}
103 104 if extra_mapping:
104 105 for k, v in extra_mapping.items():
105 106 if k not in data:
106 107 # register new mapping2lexer
107 108 data[k] = [v]
108 109
109 110 return data
110 111
111 112
112 113 def str2bool(_str):
113 114 """
114 115 returns True/False value from given string, it tries to translate the
115 116 string into boolean
116 117
117 118 :param _str: string value to translate into boolean
118 119 :rtype: boolean
119 120 :returns: boolean from given string
120 121 """
121 122 if _str is None:
122 123 return False
123 124 if _str in (True, False):
124 125 return _str
125 126 _str = str(_str).strip().lower()
126 127 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
127 128
128 129
129 130 def aslist(obj, sep=None, strip=True):
130 131 """
131 132 Returns given string separated by sep as list
132 133
133 134 :param obj:
134 135 :param sep:
135 136 :param strip:
136 137 """
137 138 if isinstance(obj, (basestring,)):
138 139 lst = obj.split(sep)
139 140 if strip:
140 141 lst = [v.strip() for v in lst]
141 142 return lst
142 143 elif isinstance(obj, (list, tuple)):
143 144 return obj
144 145 elif obj is None:
145 146 return []
146 147 else:
147 148 return [obj]
148 149
149 150
150 151 def convert_line_endings(line, mode):
151 152 """
152 153 Converts a given line "line end" accordingly to given mode
153 154
154 155 Available modes are::
155 156 0 - Unix
156 157 1 - Mac
157 158 2 - DOS
158 159
159 160 :param line: given line to convert
160 161 :param mode: mode to convert to
161 162 :rtype: str
162 163 :return: converted line according to mode
163 164 """
164 165 if mode == 0:
165 166 line = line.replace('\r\n', '\n')
166 167 line = line.replace('\r', '\n')
167 168 elif mode == 1:
168 169 line = line.replace('\r\n', '\r')
169 170 line = line.replace('\n', '\r')
170 171 elif mode == 2:
171 172 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
172 173 return line
173 174
174 175
175 176 def detect_mode(line, default):
176 177 """
177 178 Detects line break for given line, if line break couldn't be found
178 179 given default value is returned
179 180
180 181 :param line: str line
181 182 :param default: default
182 183 :rtype: int
183 184 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
184 185 """
185 186 if line.endswith('\r\n'):
186 187 return 2
187 188 elif line.endswith('\n'):
188 189 return 0
189 190 elif line.endswith('\r'):
190 191 return 1
191 192 else:
192 193 return default
193 194
194 195
195 196 def safe_int(val, default=None):
196 197 """
197 198 Returns int() of val if val is not convertable to int use default
198 199 instead
199 200
200 201 :param val:
201 202 :param default:
202 203 """
203 204
204 205 try:
205 206 val = int(val)
206 207 except (ValueError, TypeError):
207 208 val = default
208 209
209 210 return val
210 211
211 212
212 213 def safe_unicode(str_, from_encoding=None, use_chardet=False):
213 214 """
214 215 safe unicode function. Does few trick to turn str_ into unicode
215 216
216 217 In case of UnicodeDecode error, we try to return it with encoding detected
217 218 by chardet library if it fails fallback to unicode with errors replaced
218 219
219 220 :param str_: string to decode
220 221 :rtype: unicode
221 222 :returns: unicode object
222 223 """
223 224 if isinstance(str_, unicode):
224 225 return str_
225 226
226 227 if not from_encoding:
227 228 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
228 229 'utf8'), sep=',')
229 230 from_encoding = DEFAULT_ENCODINGS
230 231
231 232 if not isinstance(from_encoding, (list, tuple)):
232 233 from_encoding = [from_encoding]
233 234
234 235 try:
235 236 return unicode(str_)
236 237 except UnicodeDecodeError:
237 238 pass
238 239
239 240 for enc in from_encoding:
240 241 try:
241 242 return unicode(str_, enc)
242 243 except UnicodeDecodeError:
243 244 pass
244 245
245 246 if use_chardet:
246 247 try:
247 248 import chardet
248 249 encoding = chardet.detect(str_)['encoding']
249 250 if encoding is None:
250 251 raise Exception()
251 252 return str_.decode(encoding)
252 253 except (ImportError, UnicodeDecodeError, Exception):
253 254 return unicode(str_, from_encoding[0], 'replace')
254 255 else:
255 256 return unicode(str_, from_encoding[0], 'replace')
256 257
257 258 def safe_str(unicode_, to_encoding=None, use_chardet=False):
258 259 """
259 260 safe str function. Does few trick to turn unicode_ into string
260 261
261 262 In case of UnicodeEncodeError, we try to return it with encoding detected
262 263 by chardet library if it fails fallback to string with errors replaced
263 264
264 265 :param unicode_: unicode to encode
265 266 :rtype: str
266 267 :returns: str object
267 268 """
268 269
269 270 # if it's not basestr cast to str
270 271 if not isinstance(unicode_, compat.string_types):
271 272 return str(unicode_)
272 273
273 274 if isinstance(unicode_, str):
274 275 return unicode_
275 276
276 277 if not to_encoding:
277 278 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
278 279 'utf8'), sep=',')
279 280 to_encoding = DEFAULT_ENCODINGS
280 281
281 282 if not isinstance(to_encoding, (list, tuple)):
282 283 to_encoding = [to_encoding]
283 284
284 285 for enc in to_encoding:
285 286 try:
286 287 return unicode_.encode(enc)
287 288 except UnicodeEncodeError:
288 289 pass
289 290
290 291 if use_chardet:
291 292 try:
292 293 import chardet
293 294 encoding = chardet.detect(unicode_)['encoding']
294 295 if encoding is None:
295 296 raise UnicodeEncodeError()
296 297
297 298 return unicode_.encode(encoding)
298 299 except (ImportError, UnicodeEncodeError):
299 300 return unicode_.encode(to_encoding[0], 'replace')
300 301 else:
301 302 return unicode_.encode(to_encoding[0], 'replace')
302 303
303 304
304 305 def remove_suffix(s, suffix):
305 306 if s.endswith(suffix):
306 307 s = s[:-1 * len(suffix)]
307 308 return s
308 309
309 310
310 311 def remove_prefix(s, prefix):
311 312 if s.startswith(prefix):
312 313 s = s[len(prefix):]
313 314 return s
314 315
315 316
316 317 def find_calling_context(ignore_modules=None):
317 318 """
318 319 Look through the calling stack and return the frame which called
319 320 this function and is part of core module ( ie. rhodecode.* )
320 321
321 322 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
322 323
323 324 usage::
324 325 from rhodecode.lib.utils2 import find_calling_context
325 326
326 327 calling_context = find_calling_context(ignore_modules=[
327 328 'rhodecode.lib.caching_query',
328 329 'rhodecode.model.settings',
329 330 ])
330 331
331 332 if calling_context:
332 333 cc_str = 'call context %s:%s' % (
333 334 calling_context.f_code.co_filename,
334 335 calling_context.f_lineno,
335 336 )
336 337 print(cc_str)
337 338 """
338 339
339 340 ignore_modules = ignore_modules or []
340 341
341 342 f = sys._getframe(2)
342 343 while f.f_back is not None:
343 344 name = f.f_globals.get('__name__')
344 345 if name and name.startswith(__name__.split('.')[0]):
345 346 if name not in ignore_modules:
346 347 return f
347 348 f = f.f_back
348 349 return None
349 350
350 351
351 352 def ping_connection(connection, branch):
352 353 if branch:
353 354 # "branch" refers to a sub-connection of a connection,
354 355 # we don't want to bother pinging on these.
355 356 return
356 357
357 358 # turn off "close with result". This flag is only used with
358 359 # "connectionless" execution, otherwise will be False in any case
359 360 save_should_close_with_result = connection.should_close_with_result
360 361 connection.should_close_with_result = False
361 362
362 363 try:
363 364 # run a SELECT 1. use a core select() so that
364 365 # the SELECT of a scalar value without a table is
365 366 # appropriately formatted for the backend
366 367 connection.scalar(sqlalchemy.sql.select([1]))
367 368 except sqlalchemy.exc.DBAPIError as err:
368 369 # catch SQLAlchemy's DBAPIError, which is a wrapper
369 370 # for the DBAPI's exception. It includes a .connection_invalidated
370 371 # attribute which specifies if this connection is a "disconnect"
371 372 # condition, which is based on inspection of the original exception
372 373 # by the dialect in use.
373 374 if err.connection_invalidated:
374 375 # run the same SELECT again - the connection will re-validate
375 376 # itself and establish a new connection. The disconnect detection
376 377 # here also causes the whole connection pool to be invalidated
377 378 # so that all stale connections are discarded.
378 379 connection.scalar(sqlalchemy.sql.select([1]))
379 380 else:
380 381 raise
381 382 finally:
382 383 # restore "close with result"
383 384 connection.should_close_with_result = save_should_close_with_result
384 385
385 386
386 387 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
387 388 """Custom engine_from_config functions."""
388 389 log = logging.getLogger('sqlalchemy.engine')
389 390 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
390 391 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
391 392
392 393 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
393 394
394 395 def color_sql(sql):
395 396 color_seq = '\033[1;33m' # This is yellow: code 33
396 397 normal = '\x1b[0m'
397 398 return ''.join([color_seq, sql, normal])
398 399
399 400 if use_ping_connection:
400 401 log.debug('Adding ping_connection on the engine config.')
401 402 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
402 403
403 404 if debug:
404 405 # attach events only for debug configuration
405 406 def before_cursor_execute(conn, cursor, statement,
406 407 parameters, context, executemany):
407 408 setattr(conn, 'query_start_time', time.time())
408 409 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
409 410 calling_context = find_calling_context(ignore_modules=[
410 411 'rhodecode.lib.caching_query',
411 412 'rhodecode.model.settings',
412 413 ])
413 414 if calling_context:
414 415 log.info(color_sql('call context %s:%s' % (
415 416 calling_context.f_code.co_filename,
416 417 calling_context.f_lineno,
417 418 )))
418 419
419 420 def after_cursor_execute(conn, cursor, statement,
420 421 parameters, context, executemany):
421 422 delattr(conn, 'query_start_time')
422 423
423 424 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
424 425 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
425 426
426 427 return engine
427 428
428 429
429 430 def get_encryption_key(config):
430 431 secret = config.get('rhodecode.encrypted_values.secret')
431 432 default = config['beaker.session.secret']
432 433 return secret or default
433 434
434 435
435 436 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
436 437 short_format=False):
437 438 """
438 439 Turns a datetime into an age string.
439 440 If show_short_version is True, this generates a shorter string with
440 441 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
441 442
442 443 * IMPORTANT*
443 444 Code of this function is written in special way so it's easier to
444 445 backport it to javascript. If you mean to update it, please also update
445 446 `jquery.timeago-extension.js` file
446 447
447 448 :param prevdate: datetime object
448 449 :param now: get current time, if not define we use
449 450 `datetime.datetime.now()`
450 451 :param show_short_version: if it should approximate the date and
451 452 return a shorter string
452 453 :param show_suffix:
453 454 :param short_format: show short format, eg 2D instead of 2 days
454 455 :rtype: unicode
455 456 :returns: unicode words describing age
456 457 """
457 458
458 459 def _get_relative_delta(now, prevdate):
459 460 base = dateutil.relativedelta.relativedelta(now, prevdate)
460 461 return {
461 462 'year': base.years,
462 463 'month': base.months,
463 464 'day': base.days,
464 465 'hour': base.hours,
465 466 'minute': base.minutes,
466 467 'second': base.seconds,
467 468 }
468 469
469 470 def _is_leap_year(year):
470 471 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
471 472
472 473 def get_month(prevdate):
473 474 return prevdate.month
474 475
475 476 def get_year(prevdate):
476 477 return prevdate.year
477 478
478 479 now = now or datetime.datetime.now()
479 480 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
480 481 deltas = {}
481 482 future = False
482 483
483 484 if prevdate > now:
484 485 now_old = now
485 486 now = prevdate
486 487 prevdate = now_old
487 488 future = True
488 489 if future:
489 490 prevdate = prevdate.replace(microsecond=0)
490 491 # Get date parts deltas
491 492 for part in order:
492 493 rel_delta = _get_relative_delta(now, prevdate)
493 494 deltas[part] = rel_delta[part]
494 495
495 496 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
496 497 # not 1 hour, -59 minutes and -59 seconds)
497 498 offsets = [[5, 60], [4, 60], [3, 24]]
498 499 for element in offsets: # seconds, minutes, hours
499 500 num = element[0]
500 501 length = element[1]
501 502
502 503 part = order[num]
503 504 carry_part = order[num - 1]
504 505
505 506 if deltas[part] < 0:
506 507 deltas[part] += length
507 508 deltas[carry_part] -= 1
508 509
509 510 # Same thing for days except that the increment depends on the (variable)
510 511 # number of days in the month
511 512 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
512 513 if deltas['day'] < 0:
513 514 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
514 515 deltas['day'] += 29
515 516 else:
516 517 deltas['day'] += month_lengths[get_month(prevdate) - 1]
517 518
518 519 deltas['month'] -= 1
519 520
520 521 if deltas['month'] < 0:
521 522 deltas['month'] += 12
522 523 deltas['year'] -= 1
523 524
524 525 # Format the result
525 526 if short_format:
526 527 fmt_funcs = {
527 528 'year': lambda d: u'%dy' % d,
528 529 'month': lambda d: u'%dm' % d,
529 530 'day': lambda d: u'%dd' % d,
530 531 'hour': lambda d: u'%dh' % d,
531 532 'minute': lambda d: u'%dmin' % d,
532 533 'second': lambda d: u'%dsec' % d,
533 534 }
534 535 else:
535 536 fmt_funcs = {
536 537 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
537 538 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
538 539 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
539 540 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
540 541 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
541 542 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
542 543 }
543 544
544 545 i = 0
545 546 for part in order:
546 547 value = deltas[part]
547 548 if value != 0:
548 549
549 550 if i < 5:
550 551 sub_part = order[i + 1]
551 552 sub_value = deltas[sub_part]
552 553 else:
553 554 sub_value = 0
554 555
555 556 if sub_value == 0 or show_short_version:
556 557 _val = fmt_funcs[part](value)
557 558 if future:
558 559 if show_suffix:
559 560 return _(u'in ${ago}', mapping={'ago': _val})
560 561 else:
561 562 return _(_val)
562 563
563 564 else:
564 565 if show_suffix:
565 566 return _(u'${ago} ago', mapping={'ago': _val})
566 567 else:
567 568 return _(_val)
568 569
569 570 val = fmt_funcs[part](value)
570 571 val_detail = fmt_funcs[sub_part](sub_value)
571 572 mapping = {'val': val, 'detail': val_detail}
572 573
573 574 if short_format:
574 575 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
575 576 if show_suffix:
576 577 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
577 578 if future:
578 579 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
579 580 else:
580 581 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
581 582 if show_suffix:
582 583 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
583 584 if future:
584 585 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
585 586
586 587 return datetime_tmpl
587 588 i += 1
588 589 return _(u'just now')
589 590
590 591
591 592 def age_from_seconds(seconds):
592 593 seconds = safe_int(seconds) or 0
593 594 prevdate = time_to_datetime(time.time() + seconds)
594 595 return age(prevdate, show_suffix=False, show_short_version=True)
595 596
596 597
597 598 def cleaned_uri(uri):
598 599 """
599 600 Quotes '[' and ']' from uri if there is only one of them.
600 601 according to RFC3986 we cannot use such chars in uri
601 602 :param uri:
602 603 :return: uri without this chars
603 604 """
604 605 return urllib.quote(uri, safe='@$:/')
605 606
606 607
607 608 def credentials_filter(uri):
608 609 """
609 610 Returns a url with removed credentials
610 611
611 612 :param uri:
612 613 """
613 614 import urlobject
614 615 if isinstance(uri, rhodecode.lib.encrypt.InvalidDecryptedValue):
615 616 return 'InvalidDecryptionKey'
616 617
617 618 url_obj = urlobject.URLObject(cleaned_uri(uri))
618 619 url_obj = url_obj.without_password().without_username()
619 620
620 621 return url_obj
621 622
622 623
623 624 def get_host_info(request):
624 625 """
625 626 Generate host info, to obtain full url e.g https://server.com
626 627 use this
627 628 `{scheme}://{netloc}`
628 629 """
629 630 if not request:
630 631 return {}
631 632
632 633 qualified_home_url = request.route_url('home')
633 634 parsed_url = urlobject.URLObject(qualified_home_url)
634 635 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
635 636
636 637 return {
637 638 'scheme': parsed_url.scheme,
638 639 'netloc': parsed_url.netloc+decoded_path,
639 640 'hostname': parsed_url.hostname,
640 641 }
641 642
642 643
643 644 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
644 645 qualified_home_url = request.route_url('home')
645 646 parsed_url = urlobject.URLObject(qualified_home_url)
646 647 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
647 648
648 649 args = {
649 650 'scheme': parsed_url.scheme,
650 651 'user': '',
651 652 'sys_user': getpass.getuser(),
652 653 # path if we use proxy-prefix
653 654 'netloc': parsed_url.netloc+decoded_path,
654 655 'hostname': parsed_url.hostname,
655 656 'prefix': decoded_path,
656 657 'repo': repo_name,
657 658 'repoid': str(repo_id),
658 659 'repo_type': repo_type
659 660 }
660 661 args.update(override)
661 662 args['user'] = urllib.quote(safe_str(args['user']))
662 663
663 664 for k, v in args.items():
664 665 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
665 666
666 667 # special case for SVN clone url
667 668 if repo_type == 'svn':
668 669 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
669 670
670 671 # remove leading @ sign if it's present. Case of empty user
671 672 url_obj = urlobject.URLObject(uri_tmpl)
672 673 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
673 674
674 675 return safe_unicode(url)
675 676
676 677
677 678 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
678 679 maybe_unreachable=False, reference_obj=None):
679 680 """
680 681 Safe version of get_commit if this commit doesn't exists for a
681 682 repository it returns a Dummy one instead
682 683
683 684 :param repo: repository instance
684 685 :param commit_id: commit id as str
685 686 :param commit_idx: numeric commit index
686 687 :param pre_load: optional list of commit attributes to load
687 688 :param maybe_unreachable: translate unreachable commits on git repos
688 689 :param reference_obj: explicitly search via a reference obj in git. E.g "branch:123" would mean branch "123"
689 690 """
690 691 # TODO(skreft): remove these circular imports
691 692 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
692 693 from rhodecode.lib.vcs.exceptions import RepositoryError
693 694 if not isinstance(repo, BaseRepository):
694 695 raise Exception('You must pass an Repository '
695 696 'object as first argument got %s', type(repo))
696 697
697 698 try:
698 699 commit = repo.get_commit(
699 700 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
700 701 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
701 702 except (RepositoryError, LookupError):
702 703 commit = EmptyCommit()
703 704 return commit
704 705
705 706
706 707 def datetime_to_time(dt):
707 708 if dt:
708 709 return time.mktime(dt.timetuple())
709 710
710 711
711 712 def time_to_datetime(tm):
712 713 if tm:
713 714 if isinstance(tm, compat.string_types):
714 715 try:
715 716 tm = float(tm)
716 717 except ValueError:
717 718 return
718 719 return datetime.datetime.fromtimestamp(tm)
719 720
720 721
721 722 def time_to_utcdatetime(tm):
722 723 if tm:
723 724 if isinstance(tm, compat.string_types):
724 725 try:
725 726 tm = float(tm)
726 727 except ValueError:
727 728 return
728 729 return datetime.datetime.utcfromtimestamp(tm)
729 730
730 731
731 732 MENTIONS_REGEX = re.compile(
732 733 # ^@ or @ without any special chars in front
733 734 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
734 735 # main body starts with letter, then can be . - _
735 736 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
736 737 re.VERBOSE | re.MULTILINE)
737 738
738 739
739 740 def extract_mentioned_users(s):
740 741 """
741 742 Returns unique usernames from given string s that have @mention
742 743
743 744 :param s: string to get mentions
744 745 """
745 746 usrs = set()
746 747 for username in MENTIONS_REGEX.findall(s):
747 748 usrs.add(username)
748 749
749 750 return sorted(list(usrs), key=lambda k: k.lower())
750 751
751 752
752 753 class AttributeDictBase(dict):
753 754 def __getstate__(self):
754 755 odict = self.__dict__ # get attribute dictionary
755 756 return odict
756 757
757 758 def __setstate__(self, dict):
758 759 self.__dict__ = dict
759 760
760 761 __setattr__ = dict.__setitem__
761 762 __delattr__ = dict.__delitem__
762 763
763 764
764 765 class StrictAttributeDict(AttributeDictBase):
765 766 """
766 767 Strict Version of Attribute dict which raises an Attribute error when
767 768 requested attribute is not set
768 769 """
769 770 def __getattr__(self, attr):
770 771 try:
771 772 return self[attr]
772 773 except KeyError:
773 774 raise AttributeError('%s object has no attribute %s' % (
774 775 self.__class__, attr))
775 776
776 777
777 778 class AttributeDict(AttributeDictBase):
778 779 def __getattr__(self, attr):
779 780 return self.get(attr, None)
780 781
781 782
782 783 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
783 784 def __init__(self, default_factory=None, *args, **kwargs):
784 785 # in python3 you can omit the args to super
785 786 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
786 787 self.default_factory = default_factory
787 788
788 789
789 790 def fix_PATH(os_=None):
790 791 """
791 792 Get current active python path, and append it to PATH variable to fix
792 793 issues of subprocess calls and different python versions
793 794 """
794 795 if os_ is None:
795 796 import os
796 797 else:
797 798 os = os_
798 799
799 800 cur_path = os.path.split(sys.executable)[0]
800 801 if not os.environ['PATH'].startswith(cur_path):
801 802 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
802 803
803 804
804 805 def obfuscate_url_pw(engine):
805 806 _url = engine or ''
806 807 try:
807 808 _url = sqlalchemy.engine.url.make_url(engine)
808 809 if _url.password:
809 810 _url.password = 'XXXXX'
810 811 except Exception:
811 812 pass
812 813 return unicode(_url)
813 814
814 815
815 816 def get_server_url(environ):
816 817 req = webob.Request(environ)
817 818 return req.host_url + req.script_name
818 819
819 820
820 821 def unique_id(hexlen=32):
821 822 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
822 823 return suuid(truncate_to=hexlen, alphabet=alphabet)
823 824
824 825
825 826 def suuid(url=None, truncate_to=22, alphabet=None):
826 827 """
827 828 Generate and return a short URL safe UUID.
828 829
829 830 If the url parameter is provided, set the namespace to the provided
830 831 URL and generate a UUID.
831 832
832 833 :param url to get the uuid for
833 834 :truncate_to: truncate the basic 22 UUID to shorter version
834 835
835 836 The IDs won't be universally unique any longer, but the probability of
836 837 a collision will still be very low.
837 838 """
838 839 # Define our alphabet.
839 840 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
840 841
841 842 # If no URL is given, generate a random UUID.
842 843 if url is None:
843 844 unique_id = uuid.uuid4().int
844 845 else:
845 846 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
846 847
847 848 alphabet_length = len(_ALPHABET)
848 849 output = []
849 850 while unique_id > 0:
850 851 digit = unique_id % alphabet_length
851 852 output.append(_ALPHABET[digit])
852 853 unique_id = int(unique_id / alphabet_length)
853 854 return "".join(output)[:truncate_to]
854 855
855 856
856 857 def get_current_rhodecode_user(request=None):
857 858 """
858 859 Gets rhodecode user from request
859 860 """
860 861 pyramid_request = request or pyramid.threadlocal.get_current_request()
861 862
862 863 # web case
863 864 if pyramid_request and hasattr(pyramid_request, 'user'):
864 865 return pyramid_request.user
865 866
866 867 # api case
867 868 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
868 869 return pyramid_request.rpc_user
869 870
870 871 return None
871 872
872 873
873 874 def action_logger_generic(action, namespace=''):
874 875 """
875 876 A generic logger for actions useful to the system overview, tries to find
876 877 an acting user for the context of the call otherwise reports unknown user
877 878
878 879 :param action: logging message eg 'comment 5 deleted'
879 880 :param type: string
880 881
881 882 :param namespace: namespace of the logging message eg. 'repo.comments'
882 883 :param type: string
883 884
884 885 """
885 886
886 887 logger_name = 'rhodecode.actions'
887 888
888 889 if namespace:
889 890 logger_name += '.' + namespace
890 891
891 892 log = logging.getLogger(logger_name)
892 893
893 894 # get a user if we can
894 895 user = get_current_rhodecode_user()
895 896
896 897 logfunc = log.info
897 898
898 899 if not user:
899 900 user = '<unknown user>'
900 901 logfunc = log.warning
901 902
902 903 logfunc('Logging action by {}: {}'.format(user, action))
903 904
904 905
905 906 def escape_split(text, sep=',', maxsplit=-1):
906 907 r"""
907 908 Allows for escaping of the separator: e.g. arg='foo\, bar'
908 909
909 910 It should be noted that the way bash et. al. do command line parsing, those
910 911 single quotes are required.
911 912 """
912 913 escaped_sep = r'\%s' % sep
913 914
914 915 if escaped_sep not in text:
915 916 return text.split(sep, maxsplit)
916 917
917 918 before, _mid, after = text.partition(escaped_sep)
918 919 startlist = before.split(sep, maxsplit) # a regular split is fine here
919 920 unfinished = startlist[-1]
920 921 startlist = startlist[:-1]
921 922
922 923 # recurse because there may be more escaped separators
923 924 endlist = escape_split(after, sep, maxsplit)
924 925
925 926 # finish building the escaped value. we use endlist[0] becaue the first
926 927 # part of the string sent in recursion is the rest of the escaped value.
927 928 unfinished += sep + endlist[0]
928 929
929 930 return startlist + [unfinished] + endlist[1:] # put together all the parts
930 931
931 932
932 933 class OptionalAttr(object):
933 934 """
934 935 Special Optional Option that defines other attribute. Example::
935 936
936 937 def test(apiuser, userid=Optional(OAttr('apiuser')):
937 938 user = Optional.extract(userid)
938 939 # calls
939 940
940 941 """
941 942
942 943 def __init__(self, attr_name):
943 944 self.attr_name = attr_name
944 945
945 946 def __repr__(self):
946 947 return '<OptionalAttr:%s>' % self.attr_name
947 948
948 949 def __call__(self):
949 950 return self
950 951
951 952
952 953 # alias
953 954 OAttr = OptionalAttr
954 955
955 956
956 957 class Optional(object):
957 958 """
958 959 Defines an optional parameter::
959 960
960 961 param = param.getval() if isinstance(param, Optional) else param
961 962 param = param() if isinstance(param, Optional) else param
962 963
963 964 is equivalent of::
964 965
965 966 param = Optional.extract(param)
966 967
967 968 """
968 969
969 970 def __init__(self, type_):
970 971 self.type_ = type_
971 972
972 973 def __repr__(self):
973 974 return '<Optional:%s>' % self.type_.__repr__()
974 975
975 976 def __call__(self):
976 977 return self.getval()
977 978
978 979 def getval(self):
979 980 """
980 981 returns value from this Optional instance
981 982 """
982 983 if isinstance(self.type_, OAttr):
983 984 # use params name
984 985 return self.type_.attr_name
985 986 return self.type_
986 987
987 988 @classmethod
988 989 def extract(cls, val):
989 990 """
990 991 Extracts value from Optional() instance
991 992
992 993 :param val:
993 994 :return: original value if it's not Optional instance else
994 995 value of instance
995 996 """
996 997 if isinstance(val, cls):
997 998 return val.getval()
998 999 return val
999 1000
1000 1001
1001 1002 def glob2re(pat):
1002 1003 """
1003 1004 Translate a shell PATTERN to a regular expression.
1004 1005
1005 1006 There is no way to quote meta-characters.
1006 1007 """
1007 1008
1008 1009 i, n = 0, len(pat)
1009 1010 res = ''
1010 1011 while i < n:
1011 1012 c = pat[i]
1012 1013 i = i+1
1013 1014 if c == '*':
1014 1015 #res = res + '.*'
1015 1016 res = res + '[^/]*'
1016 1017 elif c == '?':
1017 1018 #res = res + '.'
1018 1019 res = res + '[^/]'
1019 1020 elif c == '[':
1020 1021 j = i
1021 1022 if j < n and pat[j] == '!':
1022 1023 j = j+1
1023 1024 if j < n and pat[j] == ']':
1024 1025 j = j+1
1025 1026 while j < n and pat[j] != ']':
1026 1027 j = j+1
1027 1028 if j >= n:
1028 1029 res = res + '\\['
1029 1030 else:
1030 1031 stuff = pat[i:j].replace('\\','\\\\')
1031 1032 i = j+1
1032 1033 if stuff[0] == '!':
1033 1034 stuff = '^' + stuff[1:]
1034 1035 elif stuff[0] == '^':
1035 1036 stuff = '\\' + stuff
1036 1037 res = '%s[%s]' % (res, stuff)
1037 1038 else:
1038 1039 res = res + re.escape(c)
1039 1040 return res + '\Z(?ms)'
1040 1041
1041 1042
1042 1043 def parse_byte_string(size_str):
1043 1044 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1044 1045 if not match:
1045 1046 raise ValueError('Given size:%s is invalid, please make sure '
1046 1047 'to use format of <num>(MB|KB)' % size_str)
1047 1048
1048 1049 _parts = match.groups()
1049 1050 num, type_ = _parts
1050 1051 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1051 1052
1052 1053
1053 1054 class CachedProperty(object):
1054 1055 """
1055 1056 Lazy Attributes. With option to invalidate the cache by running a method
1056 1057
1057 1058 >>> class Foo(object):
1058 1059 ...
1059 1060 ... @CachedProperty
1060 1061 ... def heavy_func(self):
1061 1062 ... return 'super-calculation'
1062 1063 ...
1063 1064 ... foo = Foo()
1064 1065 ... foo.heavy_func() # first computation
1065 1066 ... foo.heavy_func() # fetch from cache
1066 1067 ... foo._invalidate_prop_cache('heavy_func')
1067 1068
1068 1069 # at this point calling foo.heavy_func() will be re-computed
1069 1070 """
1070 1071
1071 1072 def __init__(self, func, func_name=None):
1072 1073
1073 1074 if func_name is None:
1074 1075 func_name = func.__name__
1075 1076 self.data = (func, func_name)
1076 1077 update_wrapper(self, func)
1077 1078
1078 1079 def __get__(self, inst, class_):
1079 1080 if inst is None:
1080 1081 return self
1081 1082
1082 1083 func, func_name = self.data
1083 1084 value = func(inst)
1084 1085 inst.__dict__[func_name] = value
1085 1086 if '_invalidate_prop_cache' not in inst.__dict__:
1086 1087 inst.__dict__['_invalidate_prop_cache'] = partial(
1087 1088 self._invalidate_prop_cache, inst)
1088 1089 return value
1089 1090
1090 1091 def _invalidate_prop_cache(self, inst, name):
1091 1092 inst.__dict__.pop(name, None)
1092 1093
1093 1094
1094 1095 def retry(func=None, exception=Exception, n_tries=5, delay=5, backoff=1, logger=True):
1095 1096 """
1096 1097 Retry decorator with exponential backoff.
1097 1098
1098 1099 Parameters
1099 1100 ----------
1100 1101 func : typing.Callable, optional
1101 1102 Callable on which the decorator is applied, by default None
1102 1103 exception : Exception or tuple of Exceptions, optional
1103 1104 Exception(s) that invoke retry, by default Exception
1104 1105 n_tries : int, optional
1105 1106 Number of tries before giving up, by default 5
1106 1107 delay : int, optional
1107 1108 Initial delay between retries in seconds, by default 5
1108 1109 backoff : int, optional
1109 1110 Backoff multiplier e.g. value of 2 will double the delay, by default 1
1110 1111 logger : bool, optional
1111 1112 Option to log or print, by default False
1112 1113
1113 1114 Returns
1114 1115 -------
1115 1116 typing.Callable
1116 1117 Decorated callable that calls itself when exception(s) occur.
1117 1118
1118 1119 Examples
1119 1120 --------
1120 1121 >>> import random
1121 1122 >>> @retry(exception=Exception, n_tries=3)
1122 1123 ... def test_random(text):
1123 1124 ... x = random.random()
1124 1125 ... if x < 0.5:
1125 1126 ... raise Exception("Fail")
1126 1127 ... else:
1127 1128 ... print("Success: ", text)
1128 1129 >>> test_random("It works!")
1129 1130 """
1130 1131
1131 1132 if func is None:
1132 1133 return partial(
1133 1134 retry,
1134 1135 exception=exception,
1135 1136 n_tries=n_tries,
1136 1137 delay=delay,
1137 1138 backoff=backoff,
1138 1139 logger=logger,
1139 1140 )
1140 1141
1141 1142 @wraps(func)
1142 1143 def wrapper(*args, **kwargs):
1143 1144 _n_tries, n_delay = n_tries, delay
1144 1145 log = logging.getLogger('rhodecode.retry')
1145 1146
1146 1147 while _n_tries > 1:
1147 1148 try:
1148 1149 return func(*args, **kwargs)
1149 1150 except exception as e:
1150 1151 e_details = repr(e)
1151 1152 msg = "Exception on calling func {func}: {e}, " \
1152 1153 "Retrying in {n_delay} seconds..."\
1153 1154 .format(func=func, e=e_details, n_delay=n_delay)
1154 1155 if logger:
1155 1156 log.warning(msg)
1156 1157 else:
1157 1158 print(msg)
1158 1159 time.sleep(n_delay)
1159 1160 _n_tries -= 1
1160 1161 n_delay *= backoff
1161 1162
1162 1163 return func(*args, **kwargs)
1163 1164
1164 1165 return wrapper
1165 1166
1166 1167
1167 1168 def user_agent_normalizer(user_agent_raw, safe=True):
1168 1169 log = logging.getLogger('rhodecode.user_agent_normalizer')
1169 1170 ua = (user_agent_raw or '').strip().lower()
1170 1171 ua = ua.replace('"', '')
1171 1172
1172 1173 try:
1173 1174 if 'mercurial/proto-1.0' in ua:
1174 1175 ua = ua.replace('mercurial/proto-1.0', '')
1175 1176 ua = ua.replace('(', '').replace(')', '').strip()
1176 1177 ua = ua.replace('mercurial ', 'mercurial/')
1177 1178 elif ua.startswith('git'):
1178 1179 parts = ua.split(' ')
1179 1180 if parts:
1180 1181 ua = parts[0]
1181 1182 ua = re.sub('\.windows\.\d', '', ua).strip()
1182 1183
1183 1184 return ua
1184 1185 except Exception:
1185 1186 log.exception('Failed to parse scm user-agent')
1186 1187 if not safe:
1187 1188 raise
1188 1189
1189 1190 return ua
1190 1191
1191 1192
1192 1193 def get_available_port(min_port=40000, max_port=55555):
1193 1194 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1194 1195 hostname = '127.0.0.1'
1195 1196 pick_port = min_port
1196 1197
1197 1198 for _ in range(min_port, max_port):
1198 1199 pick_port = random.randint(min_port, max_port)
1199 1200 try:
1200 1201 sock.bind((hostname, pick_port))
1201 1202 sock.close()
1202 1203 break
1203 1204 except OSError:
1204 pass
1205 continue
1206 except socket.error as e:
1207 if e.args[0] in [errno.EADDRINUSE, errno.ECONNREFUSED]:
1208 continue
1209 raise
1205 1210
1206 1211 del sock
1207 1212 return pick_port
General Comments 0
You need to be logged in to leave comments. Login now