##// END OF EJS Templates
utils: ported to python3 and new app
super-admin -
r5076:e00a2a48 default
parent child Browse files
Show More
@@ -23,7 +23,6 b' Utilities library for RhodeCode'
23 23
24 24 import datetime
25 25 import decorator
26 import json
27 26 import logging
28 27 import os
29 28 import re
@@ -34,20 +33,20 b' import tempfile'
34 33 import traceback
35 34 import tarfile
36 35 import warnings
37 import hashlib
38 36 from os.path import join as jn
39 37
40 38 import paste
41 39 import pkg_resources
42 from webhelpers2.text import collapse, remove_formatting
40 from webhelpers2.text import collapse, strip_tags, convert_accented_entities, convert_misc_entities
41
43 42 from mako import exceptions
44 from pyramid.threadlocal import get_current_registry
45 43
44 from rhodecode.lib.hash_utils import sha256_safe, md5, sha1
45 from rhodecode.lib.str_utils import safe_bytes, safe_str
46 46 from rhodecode.lib.vcs.backends.base import Config
47 47 from rhodecode.lib.vcs.exceptions import VCSError
48 48 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
49 from rhodecode.lib.utils2 import (
50 safe_str, safe_unicode, get_current_rhodecode_user, md5, sha1)
49 from rhodecode.lib.ext_json import sjson as json
51 50 from rhodecode.model import meta
52 51 from rhodecode.model.db import (
53 52 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
@@ -61,16 +60,16 b" REMOVED_REPO_PAT = re.compile(r'rm__\\d{8"
61 60 # String which contains characters that are not allowed in slug names for
62 61 # repositories or repository groups. It is properly escaped to use it in
63 62 # regular expressions.
64 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
63 SLUG_BAD_CHARS = re.escape(r'`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
65 64
66 65 # Regex that matches forbidden characters in repo/group slugs.
67 SLUG_BAD_CHAR_RE = re.compile('[{}\x00-\x08\x0b-\x0c\x0e-\x1f]'.format(SLUG_BAD_CHARS))
66 SLUG_BAD_CHAR_RE = re.compile(r'[{}\x00-\x08\x0b-\x0c\x0e-\x1f]'.format(SLUG_BAD_CHARS))
68 67
69 68 # Regex that matches allowed characters in repo/group slugs.
70 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
69 SLUG_GOOD_CHAR_RE = re.compile(r'[^{}]'.format(SLUG_BAD_CHARS))
71 70
72 71 # Regex that matches whole repo/group slugs.
73 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
72 SLUG_RE = re.compile(r'[^{}]+'.format(SLUG_BAD_CHARS))
74 73
75 74 _license_cache = None
76 75
@@ -81,12 +80,17 b' def repo_name_slug(value):'
81 80 This function is called on each creation/modification
82 81 of repository to prevent bad names in repo
83 82 """
83
84 84 replacement_char = '-'
85 85
86 slug = remove_formatting(value)
86 slug = strip_tags(value)
87 slug = convert_accented_entities(slug)
88 slug = convert_misc_entities(slug)
89
87 90 slug = SLUG_BAD_CHAR_RE.sub('', slug)
88 slug = re.sub('[\s]+', '-', slug)
91 slug = re.sub(r'[\s]+', '-', slug)
89 92 slug = collapse(slug, replacement_char)
93
90 94 return slug
91 95
92 96
@@ -96,10 +100,10 b' def repo_name_slug(value):'
96 100 def get_repo_slug(request):
97 101 _repo = ''
98 102
99 if hasattr(request, 'db_repo'):
103 if hasattr(request, 'db_repo_name'):
100 104 # if our requests has set db reference use it for name, this
101 105 # translates the example.com/_<id> into proper repo names
102 _repo = request.db_repo.repo_name
106 _repo = request.db_repo_name
103 107 elif getattr(request, 'matchdict', None):
104 108 # pyramid
105 109 _repo = request.matchdict.get('repo_name')
@@ -162,7 +166,7 b' def get_filesystem_repos(path, recursive'
162 166 log.debug('now scanning in %s location recursive:%s...', path, recursive)
163 167
164 168 def _get_repos(p):
165 dirpaths = _get_dirpaths(p)
169 dirpaths = get_dirpaths(p)
166 170 if not _is_dir_writable(p):
167 171 log.warning('repo path without write access: %s', p)
168 172
@@ -194,7 +198,7 b' def get_filesystem_repos(path, recursive'
194 198 return _get_repos(path)
195 199
196 200
197 def _get_dirpaths(p):
201 def get_dirpaths(p: str) -> list:
198 202 try:
199 203 # OS-independable way of checking if we have at least read-only
200 204 # access or not.
@@ -214,7 +218,7 b' def _get_dirpaths(p):'
214 218 def _has_correct_type(item):
215 219 if type(item) is not expected_type:
216 220 log.error(
217 "Ignoring path %s since it cannot be decoded into unicode.",
221 "Ignoring path %s since it cannot be decoded into str.",
218 222 # Using "repr" to make sure that we see the byte value in case
219 223 # of support.
220 224 repr(item))
@@ -372,7 +376,7 b' def config_data_from_db(clear_session=Tr'
372 376 log.debug(
373 377 'settings ui from db@repo[%s]: %s',
374 378 repo,
375 ','.join(map(lambda s: '[{}] {}={}'.format(*s), ui_data)))
379 ','.join(['[{}] {}={}'.format(*s) for s in ui_data]))
376 380 if clear_session:
377 381 meta.Session.remove()
378 382
@@ -441,7 +445,7 b' def set_rhodecode_config(config):'
441 445 from rhodecode.model.settings import SettingsModel
442 446 app_settings = SettingsModel().get_all_settings()
443 447
444 for k, v in app_settings.items():
448 for k, v in list(app_settings.items()):
445 449 config[k] = v
446 450
447 451
@@ -459,9 +463,9 b' def get_rhodecode_base_path():'
459 463 Returns the base path. The base path is the filesystem path which points
460 464 to the repository store.
461 465 """
462 from rhodecode.model.settings import SettingsModel
463 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
464 return safe_str(paths_ui.ui_value)
466
467 import rhodecode
468 return rhodecode.CONFIG['default_base_path']
465 469
466 470
467 471 def map_groups(path):
@@ -531,10 +535,10 b' def repo2db_mapper(initial_repo_list, re'
531 535 enable_downloads = defs.get('repo_enable_downloads')
532 536 private = defs.get('repo_private')
533 537
534 for name, repo in initial_repo_list.items():
538 for name, repo in list(initial_repo_list.items()):
535 539 group = map_groups(name)
536 unicode_name = safe_unicode(name)
537 db_repo = repo_model.get_by_repo_name(unicode_name)
540 str_name = safe_str(name)
541 db_repo = repo_model.get_by_repo_name(str_name)
538 542 # found repo that is on filesystem not in RhodeCode database
539 543 if not db_repo:
540 544 log.info('repository %s not found, creating now', name)
@@ -574,7 +578,7 b' def repo2db_mapper(initial_repo_list, re'
574 578 if remove_obsolete:
575 579 # remove from database those repositories that are not in the filesystem
576 580 for repo in sa.query(Repository).all():
577 if repo.repo_name not in initial_repo_list.keys():
581 if repo.repo_name not in list(initial_repo_list.keys()):
578 582 log.debug("Removing non-existing repository found in db `%s`",
579 583 repo.repo_name)
580 584 try:
@@ -594,13 +598,14 b' def repo2db_mapper(initial_repo_list, re'
594 598 return gr_name
595 599
596 600 initial_repo_group_list = [splitter(x) for x in
597 initial_repo_list.keys() if splitter(x)]
601 list(initial_repo_list.keys()) if splitter(x)]
598 602
599 603 # remove from database those repository groups that are not in the
600 604 # filesystem due to parent child relationships we need to delete them
601 605 # in a specific order of most nested first
602 606 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
603 nested_sort = lambda gr: len(gr.split('/'))
607 def nested_sort(gr):
608 return len(gr.split('/'))
604 609 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
605 610 if group_name not in initial_repo_group_list:
606 611 repo_group = RepoGroup.get_by_group_name(group_name)
@@ -638,7 +643,7 b' def load_rcextensions(root_path):'
638 643 rcextensions = __import__('rcextensions')
639 644 except ImportError:
640 645 if os.path.isdir(os.path.join(path, 'rcextensions')):
641 log.warn('Unable to load rcextensions from %s', path)
646 log.warning('Unable to load rcextensions from %s', path)
642 647 rcextensions = None
643 648
644 649 if rcextensions:
@@ -676,8 +681,11 b' def create_test_index(repo_location, con'
676 681 """
677 682 Makes default test index.
678 683 """
679 import rc_testdata
680
684 try:
685 import rc_testdata
686 except ImportError:
687 raise ImportError('Failed to import rc_testdata, '
688 'please make sure this package is installed from requirements_test.txt')
681 689 rc_testdata.extract_search_index(
682 690 'vcs_search_index', os.path.dirname(config['search.location']))
683 691
@@ -696,13 +704,16 b' def create_test_database(test_path, conf'
696 704 Makes a fresh database.
697 705 """
698 706 from rhodecode.lib.db_manage import DbManage
707 from rhodecode.lib.utils2 import get_encryption_key
699 708
700 709 # PART ONE create db
701 710 dbconf = config['sqlalchemy.db1.url']
711 enc_key = get_encryption_key(config)
712
702 713 log.debug('making test db %s', dbconf)
703 714
704 715 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
705 tests=True, cli_args={'force_ask': True})
716 tests=True, cli_args={'force_ask': True}, enc_key=enc_key)
706 717 dbmanage.create_tables(override=True)
707 718 dbmanage.set_db_version()
708 719 # for tests dynamically set new root paths based on generated content
@@ -752,7 +763,7 b' def password_changed(auth_user, session)'
752 763 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
753 764 return False
754 765
755 password_hash = md5(auth_user.password) if auth_user.password else None
766 password_hash = md5(safe_bytes(auth_user.password)) if auth_user.password else None
756 767 rhodecode_user = session.get('rhodecode_user', {})
757 768 session_password_hash = rhodecode_user.get('password', '')
758 769 return password_hash != session_password_hash
@@ -777,7 +788,7 b' def generate_platform_uuid():'
777 788
778 789 try:
779 790 uuid_list = [platform.platform()]
780 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
791 return sha256_safe(':'.join(uuid_list))
781 792 except Exception as e:
782 793 log.error('Failed to generate host uuid: %s', e)
783 794 return 'UNDEFINED'
@@ -39,27 +39,23 b' import getpass'
39 39 import socket
40 40 import errno
41 41 import random
42 from functools import update_wrapper, partial, wraps
42 import functools
43 43 from contextlib import closing
44 44
45 45 import pygments.lexers
46 46 import sqlalchemy
47 import sqlalchemy.event
47 48 import sqlalchemy.engine.url
48 49 import sqlalchemy.exc
49 50 import sqlalchemy.sql
50 51 import webob
51 import pyramid.threadlocal
52 52 from pyramid.settings import asbool
53 53
54 54 import rhodecode
55 55 from rhodecode.translation import _, _pluralize
56 56 from rhodecode.lib.str_utils import safe_str, safe_int, safe_bytes
57 57 from rhodecode.lib.hash_utils import md5, md5_safe, sha1, sha1_safe
58 from rhodecode.lib.type_utils import aslist, str2bool
59 from functools import reduce
60
61 #TODO: there's no longer safe_unicode, we mock it now, but should remove it
62 safe_unicode = safe_str
58 from rhodecode.lib.type_utils import aslist, str2bool, StrictAttributeDict, AttributeDict
63 59
64 60
65 61 def __get_lem(extra_mapping=None):
@@ -85,7 +81,7 b' def __get_lem(extra_mapping=None):'
85 81 for lx, t in sorted(pygments.lexers.LEXERS.items()):
86 82 m = list(map(__clean, t[-2]))
87 83 if m:
88 m = reduce(lambda x, y: x + y, m)
84 m = functools.reduce(lambda x, y: x + y, m)
89 85 for ext in m:
90 86 desc = lx.replace('Lexer', '')
91 87 d[ext].append(desc)
@@ -94,7 +90,7 b' def __get_lem(extra_mapping=None):'
94 90
95 91 extra_mapping = extra_mapping or {}
96 92 if extra_mapping:
97 for k, v in extra_mapping.items():
93 for k, v in list(extra_mapping.items()):
98 94 if k not in data:
99 95 # register new mapping2lexer
100 96 data[k] = [v]
@@ -102,7 +98,7 b' def __get_lem(extra_mapping=None):'
102 98 return data
103 99
104 100
105 def convert_line_endings(line, mode):
101 def convert_line_endings(line: str, mode) -> str:
106 102 """
107 103 Converts a given line "line end" accordingly to given mode
108 104
@@ -113,7 +109,6 b' def convert_line_endings(line, mode):'
113 109
114 110 :param line: given line to convert
115 111 :param mode: mode to convert to
116 :rtype: str
117 112 :return: converted line according to mode
118 113 """
119 114 if mode == 0:
@@ -127,14 +122,13 b' def convert_line_endings(line, mode):'
127 122 return line
128 123
129 124
130 def detect_mode(line, default):
125 def detect_mode(line: str, default) -> int:
131 126 """
132 127 Detects line break for given line, if line break couldn't be found
133 128 given default value is returned
134 129
135 130 :param line: str line
136 131 :param default: default
137 :rtype: int
138 132 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
139 133 """
140 134 if line.endswith('\r\n'):
@@ -159,14 +153,18 b' def remove_prefix(s, prefix):'
159 153 return s
160 154
161 155
162 def find_calling_context(ignore_modules=None):
156 def find_calling_context(ignore_modules=None, depth=4, output_writer=None, indent=True):
163 157 """
164 158 Look through the calling stack and return the frame which called
165 159 this function and is part of core module ( ie. rhodecode.* )
166 160
167 161 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
162 :param depth:
163 :param output_writer:
164 :param indent:
168 165
169 166 usage::
167
170 168 from rhodecode.lib.utils2 import find_calling_context
171 169
172 170 calling_context = find_calling_context(ignore_modules=[
@@ -174,24 +172,36 b' def find_calling_context(ignore_modules='
174 172 'rhodecode.model.settings',
175 173 ])
176 174
177 if calling_context:
178 cc_str = 'call context %s:%s' % (
179 calling_context.f_code.co_filename,
180 calling_context.f_lineno,
181 )
182 print(cc_str)
183 175 """
176 import inspect
177 if not output_writer:
178 try:
179 from rich import print as pprint
180 except ImportError:
181 pprint = print
182 output_writer = pprint
184 183
185 ignore_modules = ignore_modules or []
184 frame = inspect.currentframe()
185 cc = []
186 try:
187 for i in range(depth): # current frame + 3 callers
188 frame = frame.f_back
189 if not frame:
190 break
186 191
187 f = sys._getframe(2)
188 while f.f_back is not None:
189 name = f.f_globals.get('__name__')
190 if name and name.startswith(__name__.split('.')[0]):
192 info = inspect.getframeinfo(frame)
193 name = frame.f_globals.get('__name__')
191 194 if name not in ignore_modules:
192 return f
193 f = f.f_back
194 return None
195 cc.insert(0, f'CALL_CONTEXT:{i}: file {info.filename}:{info.lineno} -> {info.function}')
196 finally:
197 # Avoids a reference cycle
198 del frame
199
200 output_writer('* INFO: This code was called from: *')
201 for cnt, frm_info in enumerate(cc):
202 if not indent:
203 cnt = 1
204 output_writer(' ' * cnt + frm_info)
195 205
196 206
197 207 def ping_connection(connection, branch):
@@ -252,15 +262,10 b' def engine_from_config(configuration, pr'
252 262 parameters, context, executemany):
253 263 setattr(conn, 'query_start_time', time.time())
254 264 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
255 calling_context = find_calling_context(ignore_modules=[
265 find_calling_context(ignore_modules=[
256 266 'rhodecode.lib.caching_query',
257 267 'rhodecode.model.settings',
258 ])
259 if calling_context:
260 log.info(color_sql('call context %s:%s' % (
261 calling_context.f_code.co_filename,
262 calling_context.f_lineno,
263 )))
268 ], output_writer=log.info)
264 269
265 270 def after_cursor_execute(conn, cursor, statement,
266 271 parameters, context, executemany):
@@ -272,10 +277,12 b' def engine_from_config(configuration, pr'
272 277 return engine
273 278
274 279
275 def get_encryption_key(config):
280 def get_encryption_key(config) -> bytes:
276 281 secret = config.get('rhodecode.encrypted_values.secret')
277 282 default = config['beaker.session.secret']
278 return secret or default
283 enc_key = secret or default
284
285 return safe_bytes(enc_key)
279 286
280 287
281 288 def age(prevdate, now=None, show_short_version=False, show_suffix=True, short_format=False):
@@ -476,7 +483,7 b' def get_host_info(request):'
476 483
477 484 qualified_home_url = request.route_url('home')
478 485 parsed_url = urlobject.URLObject(qualified_home_url)
479 decoded_path = safe_unicode(urllib.parse.unquote(parsed_url.path.rstrip('/')))
486 decoded_path = safe_str(urllib.parse.unquote(parsed_url.path.rstrip('/')))
480 487
481 488 return {
482 489 'scheme': parsed_url.scheme,
@@ -488,7 +495,7 b' def get_host_info(request):'
488 495 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
489 496 qualified_home_url = request.route_url('home')
490 497 parsed_url = urlobject.URLObject(qualified_home_url)
491 decoded_path = safe_unicode(urllib.parse.unquote(parsed_url.path.rstrip('/')))
498 decoded_path = safe_str(urllib.parse.unquote(parsed_url.path.rstrip('/')))
492 499
493 500 args = {
494 501 'scheme': parsed_url.scheme,
@@ -505,8 +512,9 b' def get_clone_url(request, uri_tmpl, rep'
505 512 args.update(override)
506 513 args['user'] = urllib.parse.quote(safe_str(args['user']))
507 514
508 for k, v in args.items():
509 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
515 for k, v in list(args.items()):
516 tmpl_key = '{%s}' % k
517 uri_tmpl = uri_tmpl.replace(tmpl_key, v)
510 518
511 519 # special case for SVN clone url
512 520 if repo_type == 'svn':
@@ -516,7 +524,7 b' def get_clone_url(request, uri_tmpl, rep'
516 524 url_obj = urlobject.URLObject(uri_tmpl)
517 525 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
518 526
519 return safe_unicode(url)
527 return safe_str(url)
520 528
521 529
522 530 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
@@ -594,36 +602,6 b' def extract_mentioned_users(s):'
594 602 return sorted(list(usrs), key=lambda k: k.lower())
595 603
596 604
597 class AttributeDictBase(dict):
598 def __getstate__(self):
599 odict = self.__dict__ # get attribute dictionary
600 return odict
601
602 def __setstate__(self, dict):
603 self.__dict__ = dict
604
605 __setattr__ = dict.__setitem__
606 __delattr__ = dict.__delitem__
607
608
609 class StrictAttributeDict(AttributeDictBase):
610 """
611 Strict Version of Attribute dict which raises an Attribute error when
612 requested attribute is not set
613 """
614 def __getattr__(self, attr):
615 try:
616 return self[attr]
617 except KeyError:
618 raise AttributeError('%s object has no attribute %s' % (
619 self.__class__, attr))
620
621
622 class AttributeDict(AttributeDictBase):
623 def __getattr__(self, attr):
624 return self.get(attr, None)
625
626
627 605 def fix_PATH(os_=None):
628 606 """
629 607 Get current active python path, and append it to PATH variable to fix
@@ -635,19 +613,18 b' def fix_PATH(os_=None):'
635 613 os = os_
636 614
637 615 cur_path = os.path.split(sys.executable)[0]
616 os_path = os.environ['PATH']
638 617 if not os.environ['PATH'].startswith(cur_path):
639 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
618 os.environ['PATH'] = f'{cur_path}:{os_path}'
640 619
641 620
642 621 def obfuscate_url_pw(engine):
643 622 _url = engine or ''
644 623 try:
645 624 _url = sqlalchemy.engine.url.make_url(engine)
646 if _url.password:
647 _url.password = 'XXXXX'
648 625 except Exception:
649 626 pass
650 return str(_url)
627 return repr(_url)
651 628
652 629
653 630 def get_server_url(environ):
@@ -695,6 +672,7 b' def get_current_rhodecode_user(request=N'
695 672 """
696 673 Gets rhodecode user from request
697 674 """
675 import pyramid.threadlocal
698 676 pyramid_request = request or pyramid.threadlocal.get_current_request()
699 677
700 678 # web case
@@ -837,51 +815,15 b' class Optional(object):'
837 815
838 816
839 817 def glob2re(pat):
840 """
841 Translate a shell PATTERN to a regular expression.
842
843 There is no way to quote meta-characters.
844 """
845
846 i, n = 0, len(pat)
847 res = ''
848 while i < n:
849 c = pat[i]
850 i = i+1
851 if c == '*':
852 #res = res + '.*'
853 res = res + '[^/]*'
854 elif c == '?':
855 #res = res + '.'
856 res = res + '[^/]'
857 elif c == '[':
858 j = i
859 if j < n and pat[j] == '!':
860 j = j+1
861 if j < n and pat[j] == ']':
862 j = j+1
863 while j < n and pat[j] != ']':
864 j = j+1
865 if j >= n:
866 res = res + '\\['
867 else:
868 stuff = pat[i:j].replace('\\','\\\\')
869 i = j+1
870 if stuff[0] == '!':
871 stuff = '^' + stuff[1:]
872 elif stuff[0] == '^':
873 stuff = '\\' + stuff
874 res = '%s[%s]' % (res, stuff)
875 else:
876 res = res + re.escape(c)
877 return res + '\Z(?ms)'
818 import fnmatch
819 return fnmatch.translate(pat)
878 820
879 821
880 822 def parse_byte_string(size_str):
881 823 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
882 824 if not match:
883 raise ValueError('Given size:%s is invalid, please make sure '
884 'to use format of <num>(MB|KB)' % size_str)
825 raise ValueError(f'Given size:{size_str} is invalid, please make sure '
826 f'to use format of <num>(MB|KB)')
885 827
886 828 _parts = match.groups()
887 829 num, type_ = _parts
@@ -911,7 +853,7 b' class CachedProperty(object):'
911 853 if func_name is None:
912 854 func_name = func.__name__
913 855 self.data = (func, func_name)
914 update_wrapper(self, func)
856 functools.update_wrapper(self, func)
915 857
916 858 def __get__(self, inst, class_):
917 859 if inst is None:
@@ -921,7 +863,7 b' class CachedProperty(object):'
921 863 value = func(inst)
922 864 inst.__dict__[func_name] = value
923 865 if '_invalidate_prop_cache' not in inst.__dict__:
924 inst.__dict__['_invalidate_prop_cache'] = partial(
866 inst.__dict__['_invalidate_prop_cache'] = functools.partial(
925 867 self._invalidate_prop_cache, inst)
926 868 return value
927 869
@@ -967,7 +909,7 b' def retry(func=None, exception=Exception'
967 909 """
968 910
969 911 if func is None:
970 return partial(
912 return functools.partial(
971 913 retry,
972 914 exception=exception,
973 915 n_tries=n_tries,
@@ -976,7 +918,7 b' def retry(func=None, exception=Exception'
976 918 logger=logger,
977 919 )
978 920
979 @wraps(func)
921 @functools.wraps(func)
980 922 def wrapper(*args, **kwargs):
981 923 _n_tries, n_delay = n_tries, delay
982 924 log = logging.getLogger('rhodecode.retry')
@@ -1016,7 +958,7 b' def user_agent_normalizer(user_agent_raw'
1016 958 parts = ua.split(' ')
1017 959 if parts:
1018 960 ua = parts[0]
1019 ua = re.sub('\.windows\.\d', '', ua).strip()
961 ua = re.sub(r'\.windows\.\d', '', ua).strip()
1020 962
1021 963 return ua
1022 964 except Exception:
General Comments 0
You need to be logged in to leave comments. Login now