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