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