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