Initial import from Docker volume
This commit is contained in:
97
tools/__init__.py
Executable file
97
tools/__init__.py
Executable file
@@ -0,0 +1,97 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import posixpath
|
||||
from functools import cached_property
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
try:
|
||||
from werkzeug.security import safe_join
|
||||
except ImportError:
|
||||
safe_join = posixpath.join
|
||||
|
||||
from .decimal_ import decistmt
|
||||
from .misc import (
|
||||
entry_points, escape_wildcard, file_open, find_dir, find_path, firstline,
|
||||
get_smtp_server, grouped_slice, import_module, is_full_text,
|
||||
is_instance_method, likify, lstrip_wildcard, pairwise_longest,
|
||||
reduce_domain, reduce_ids, remove_forbidden_chars, resolve,
|
||||
rstrip_wildcard, slugify, sortable_values, sql_pairing, strip_wildcard,
|
||||
unescape_wildcard)
|
||||
|
||||
_NOT_FOUND = object()
|
||||
|
||||
|
||||
class cached_property(cached_property):
|
||||
_cache = None
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
cache = getattr(instance, '__dict__', None)
|
||||
if cache is None: # __slots__
|
||||
if self._cache is None:
|
||||
self._cache = WeakKeyDictionary()
|
||||
val = self._cache.get(instance, _NOT_FOUND)
|
||||
if val is _NOT_FOUND:
|
||||
self._cache[instance] = val = self.func(instance)
|
||||
return val
|
||||
else:
|
||||
return super().__get__(instance, owner)
|
||||
|
||||
def __delete__(self, instance):
|
||||
cache = getattr(instance, '__dict__', None)
|
||||
try:
|
||||
if cache:
|
||||
del cache[self.attrname]
|
||||
elif self._cache:
|
||||
del self._cache[instance]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
class ClassProperty(property):
|
||||
def __get__(self, cls, owner):
|
||||
return self.fget.__get__(None, owner)()
|
||||
|
||||
|
||||
def cursor_dict(cursor, size=None):
|
||||
size = cursor.arraysize if size is None else size
|
||||
while True:
|
||||
rows = cursor.fetchmany(size)
|
||||
if not rows:
|
||||
break
|
||||
for row in rows:
|
||||
yield {d[0]: v for d, v in zip(cursor.description, row)}
|
||||
|
||||
|
||||
__all__ = [
|
||||
ClassProperty,
|
||||
cached_property,
|
||||
cursor_dict,
|
||||
decistmt,
|
||||
entry_points,
|
||||
escape_wildcard,
|
||||
file_open,
|
||||
find_dir,
|
||||
find_path,
|
||||
firstline,
|
||||
get_smtp_server,
|
||||
grouped_slice,
|
||||
import_module,
|
||||
is_full_text,
|
||||
is_instance_method,
|
||||
likify,
|
||||
lstrip_wildcard,
|
||||
pairwise_longest,
|
||||
reduce_domain,
|
||||
reduce_ids,
|
||||
remove_forbidden_chars,
|
||||
resolve,
|
||||
rstrip_wildcard,
|
||||
safe_join,
|
||||
slugify,
|
||||
sortable_values,
|
||||
sql_pairing,
|
||||
strip_wildcard,
|
||||
unescape_wildcard,
|
||||
]
|
||||
BIN
tools/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/__init__.cpython-311.pyc
Executable file
BIN
tools/__pycache__/__init__.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/barcode.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/barcode.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/barcode.cpython-311.pyc
Executable file
BIN
tools/__pycache__/barcode.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/decimal_.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/decimal_.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/decimal_.cpython-311.pyc
Executable file
BIN
tools/__pycache__/decimal_.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/domain_inversion.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/domain_inversion.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/domain_inversion.cpython-311.pyc
Executable file
BIN
tools/__pycache__/domain_inversion.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/email_.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/email_.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/email_.cpython-311.pyc
Executable file
BIN
tools/__pycache__/email_.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/gevent.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/gevent.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/gevent.cpython-311.pyc
Executable file
BIN
tools/__pycache__/gevent.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/immutabledict.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/immutabledict.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/immutabledict.cpython-311.pyc
Executable file
BIN
tools/__pycache__/immutabledict.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/logging.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/logging.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/logging.cpython-311.pyc
Executable file
BIN
tools/__pycache__/logging.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/misc.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/misc.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/misc.cpython-311.pyc
Executable file
BIN
tools/__pycache__/misc.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/qrcode.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/qrcode.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/qrcode.cpython-311.pyc
Executable file
BIN
tools/__pycache__/qrcode.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/singleton.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/singleton.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/singleton.cpython-311.pyc
Executable file
BIN
tools/__pycache__/singleton.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/string_.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/string_.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/string_.cpython-311.pyc
Executable file
BIN
tools/__pycache__/string_.cpython-311.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/timezone.cpython-311.opt-1.pyc
Executable file
BIN
tools/__pycache__/timezone.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
tools/__pycache__/timezone.cpython-311.pyc
Executable file
BIN
tools/__pycache__/timezone.cpython-311.pyc
Executable file
Binary file not shown.
67
tools/barcode.py
Executable file
67
tools/barcode.py
Executable file
@@ -0,0 +1,67 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import barcode
|
||||
from barcode import PROVIDED_BARCODES as BARCODES
|
||||
from barcode.writer import ImageWriter, SVGWriter
|
||||
|
||||
__all__ = ['BARCODES', 'generate_svg', 'generate_png']
|
||||
|
||||
BARCODES = set(BARCODES) | {'ean', 'isbn'}
|
||||
|
||||
|
||||
def _generate(name, code, writer, **options):
|
||||
output = BytesIO()
|
||||
if name == 'ean':
|
||||
name = {
|
||||
14: 'ean14',
|
||||
13: 'ean13',
|
||||
12: 'upc',
|
||||
8: 'ean8',
|
||||
}[len(code)]
|
||||
elif name == 'isbn':
|
||||
name = {
|
||||
13: 'isbn13',
|
||||
10: 'isbn10',
|
||||
}[len(code)]
|
||||
Generator = barcode.get(name)
|
||||
Generator(code, writer=writer).write(output, options=options)
|
||||
return output
|
||||
|
||||
|
||||
def generate_svg(
|
||||
name, code, width=0.2, height=15.0, border=6.5,
|
||||
font_size=10, text_distance=5.0,
|
||||
background='white', foreground='black'):
|
||||
writer = SVGWriter()
|
||||
options = dict(
|
||||
module_width=width,
|
||||
module_height=height,
|
||||
quiet_zone=border,
|
||||
font_size=font_size,
|
||||
text_distance=text_distance,
|
||||
background=background,
|
||||
foreground=foreground)
|
||||
return _generate(name, code, writer, **options)
|
||||
|
||||
|
||||
def generate_png(
|
||||
name, code, width=2, height=150, border=6.5,
|
||||
font_size=10, text_distance=5.0,
|
||||
background='white', foreground='black'):
|
||||
dpi = 300
|
||||
width = width * 25.4 / dpi
|
||||
height = height * 25.4 / dpi
|
||||
writer = ImageWriter()
|
||||
options = dict(
|
||||
format='png',
|
||||
module_with=width,
|
||||
module_height=height,
|
||||
quiet_zone=border,
|
||||
font_size=font_size,
|
||||
text_distance=text_distance,
|
||||
background=background,
|
||||
foreground=foreground)
|
||||
return _generate(name, code, writer, **options)
|
||||
47
tools/decimal_.py
Executable file
47
tools/decimal_.py
Executable file
@@ -0,0 +1,47 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
from io import BytesIO
|
||||
from tokenize import NAME, NUMBER, OP, STRING, tokenize, untokenize
|
||||
|
||||
# code snippet taken from http://docs.python.org/library/tokenize.html
|
||||
|
||||
|
||||
def decistmt(s):
|
||||
"""Substitute Decimals for floats or integers in a string of statements.
|
||||
|
||||
>>> from decimal import Decimal
|
||||
>>> s = 'print(+21.3e-5*-.1234/81.7)'
|
||||
>>> decistmt(s)
|
||||
"print (+Decimal ('21.3e-5')*-Decimal ('.1234')/Decimal ('81.7'))"
|
||||
|
||||
The format of the exponent is inherited from the platform C library.
|
||||
Known cases are "e-007" (Windows) and "e-07" (not Windows). Since
|
||||
we're only showing 12 digits, and the 13th isn't close to 5, the
|
||||
rest of the output should be platform-independent.
|
||||
|
||||
>>> exec(s) #doctest: +ELLIPSIS
|
||||
-3.217160342717258e-0...7
|
||||
|
||||
Output from calculations with Decimal should be identical across all
|
||||
platforms.
|
||||
|
||||
>>> exec(decistmt(s))
|
||||
-3.217160342717258261933904529E-7
|
||||
>>> decistmt('0')
|
||||
"Decimal ('0')"
|
||||
>>> decistmt('1.23')
|
||||
"Decimal ('1.23')"
|
||||
"""
|
||||
result = []
|
||||
g = tokenize(BytesIO(s.encode('utf-8')).readline) # tokenize the string
|
||||
for toknum, tokval, _, _, _ in g:
|
||||
if toknum == NUMBER: # replace NUMBER tokens
|
||||
result.extend([
|
||||
(NAME, 'Decimal'),
|
||||
(OP, '('),
|
||||
(STRING, repr(tokval)),
|
||||
(OP, ')')
|
||||
])
|
||||
else:
|
||||
result.append((toknum, tokval))
|
||||
return untokenize(result).decode('utf-8')
|
||||
557
tools/domain_inversion.py
Executable file
557
tools/domain_inversion.py
Executable file
@@ -0,0 +1,557 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
import datetime
|
||||
import operator
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from functools import partial, reduce
|
||||
|
||||
|
||||
def sql_like(value, pattern, ignore_case=True):
|
||||
flag = re.IGNORECASE if ignore_case else 0
|
||||
|
||||
escape = False
|
||||
chars = []
|
||||
for char in re.split(r'(\\|.)', pattern)[1::2]:
|
||||
if escape:
|
||||
if char in ('%', '_'):
|
||||
chars.append(char)
|
||||
else:
|
||||
chars.extend(['\\', char])
|
||||
escape = False
|
||||
elif char == '\\':
|
||||
escape = True
|
||||
elif char == '_':
|
||||
chars.append('.')
|
||||
elif char == '%':
|
||||
chars.append('.*')
|
||||
else:
|
||||
chars.append(re.escape(char))
|
||||
|
||||
regexp = re.compile(''.join(chars), flag)
|
||||
return bool(regexp.fullmatch(value))
|
||||
|
||||
|
||||
like = partial(sql_like, ignore_case=False)
|
||||
ilike = partial(sql_like, ignore_case=True)
|
||||
|
||||
|
||||
def in_(a, b):
|
||||
if isinstance(a, (list, tuple)):
|
||||
if isinstance(b, (list, tuple)):
|
||||
return any(operator.contains(b, x) for x in a)
|
||||
else:
|
||||
return operator.contains(a, b)
|
||||
else:
|
||||
return operator.contains(b, a)
|
||||
|
||||
|
||||
OPERATORS = defaultdict(lambda: lambda a, b: True)
|
||||
OPERATORS.update({
|
||||
'=': operator.eq,
|
||||
'>': operator.gt,
|
||||
'<': operator.lt,
|
||||
'<=': operator.le,
|
||||
'>=': operator.ge,
|
||||
'!=': operator.ne,
|
||||
'in': in_,
|
||||
'not in': lambda a, b: not in_(a, b),
|
||||
'ilike': ilike,
|
||||
'not ilike': lambda a, b: not ilike(a, b),
|
||||
'like': like,
|
||||
'not like': lambda a, b: not like(a, b),
|
||||
})
|
||||
|
||||
|
||||
def locale_part(expression, field_name, locale_name='id'):
|
||||
if expression == field_name:
|
||||
return locale_name
|
||||
if '.' in expression:
|
||||
fieldname, local = expression.split('.', 1)
|
||||
return local
|
||||
return expression
|
||||
|
||||
|
||||
def is_leaf(expression):
|
||||
return (isinstance(expression, (list, tuple))
|
||||
and len(expression) > 2
|
||||
and isinstance(expression[1], str))
|
||||
|
||||
|
||||
def constrained_leaf(part, boolop=operator.and_):
|
||||
field, operand, value = part[:3]
|
||||
if operand == '=' and boolop == operator.and_:
|
||||
# We should consider that other domain inversion will set a correct
|
||||
# value to this field
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def eval_leaf(part, context, boolop=operator.and_):
|
||||
field, operand, value = part[:3]
|
||||
if '.' in field:
|
||||
# In the case where the leaf concerns a m2o then having a value in the
|
||||
# evaluation context is deemed suffisant
|
||||
return bool(context.get(field.split('.')[0]))
|
||||
context_field = context.get(field)
|
||||
if (operand not in {'=', '!='}
|
||||
and (context_field is None or value is None)
|
||||
and not (operand in {'in', 'not in'}
|
||||
and context_field is None
|
||||
and (isinstance(value, (list, tuple)) and None in value))):
|
||||
return
|
||||
if isinstance(context_field, datetime.date) and not value:
|
||||
if isinstance(context_field, datetime.datetime):
|
||||
value = datetime.datetime.min
|
||||
else:
|
||||
value = datetime.date.min
|
||||
if isinstance(value, datetime.date) and not context_field:
|
||||
if isinstance(value, datetime.datetime):
|
||||
context_field = datetime.datetime.min
|
||||
else:
|
||||
context_field = datetime.date.min
|
||||
if isinstance(context_field, (list, tuple)) and value is None:
|
||||
value = type(context_field)()
|
||||
if (isinstance(context_field, str)
|
||||
and isinstance(value, (list, tuple))):
|
||||
try:
|
||||
value = '%s,%s' % tuple(value)
|
||||
except TypeError:
|
||||
pass
|
||||
elif (isinstance(context_field, (list, tuple))
|
||||
and isinstance(value, str)):
|
||||
try:
|
||||
context_field = '%s,%s' % tuple(context_field)
|
||||
except TypeError:
|
||||
pass
|
||||
elif ((isinstance(context_field, list) and isinstance(value, tuple))
|
||||
or (isinstance(context_field, tuple) and isinstance(value, list))):
|
||||
context_field = list(context_field)
|
||||
value = list(value)
|
||||
if (operand in ('=', '!=')
|
||||
and isinstance(context_field, (list, tuple))
|
||||
and isinstance(value, int)):
|
||||
operand = {
|
||||
'=': 'in',
|
||||
'!=': 'not in',
|
||||
}[operand]
|
||||
try:
|
||||
return OPERATORS[operand](context_field, value)
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
|
||||
def inverse_leaf(domain):
|
||||
if domain in ('AND', 'OR'):
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
if 'child_of' in domain[1] and '.' not in domain[0]:
|
||||
if len(domain) == 3:
|
||||
return domain
|
||||
else:
|
||||
return [domain[3]] + list(domain[1:])
|
||||
return domain
|
||||
else:
|
||||
return list(map(inverse_leaf, domain))
|
||||
|
||||
|
||||
def filter_leaf(domain, field, model):
|
||||
if domain in ('AND', 'OR'):
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
if domain[0].startswith(field) and len(domain) > 3:
|
||||
if domain[3] != model:
|
||||
return ('id', '=', None)
|
||||
return domain
|
||||
else:
|
||||
return [filter_leaf(d, field, model) for d in domain]
|
||||
|
||||
|
||||
def prepare_reference_domain(domain, reference):
|
||||
"convert domain to replace reference fields by their local part"
|
||||
|
||||
def value2reference(value):
|
||||
model, ref_id = None, None
|
||||
if isinstance(value, str) and ',' in value:
|
||||
model, ref_id = value.split(',', 1)
|
||||
if ref_id != '%':
|
||||
try:
|
||||
ref_id = int(ref_id)
|
||||
except ValueError:
|
||||
model, ref_id = None, value
|
||||
elif (isinstance(value, (list, tuple))
|
||||
and len(value) == 2
|
||||
and isinstance(value[0], str)
|
||||
and (isinstance(value[1], int) or value[1] == '%')):
|
||||
model, ref_id = value
|
||||
else:
|
||||
ref_id = value
|
||||
return model, ref_id
|
||||
|
||||
if domain in ('AND', 'OR'):
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
if domain[0] == reference:
|
||||
if domain[1] in {'=', '!='}:
|
||||
model, ref_id = value2reference(domain[2])
|
||||
if model is not None:
|
||||
if ref_id == '%':
|
||||
if domain[1] == '=':
|
||||
return [reference + '.id', '!=', None, model]
|
||||
else:
|
||||
return [reference, 'not like', domain[2]]
|
||||
return [reference + '.id', domain[1], ref_id, model]
|
||||
elif domain[1] in {'in', 'not in'}:
|
||||
model_values = {}
|
||||
for value in domain[2]:
|
||||
model, ref_id = value2reference(value)
|
||||
if model is None:
|
||||
break
|
||||
model_values.setdefault(model, []).append(ref_id)
|
||||
else:
|
||||
new_domain = ['OR'] if domain[1] == 'in' else ['AND']
|
||||
for model, ref_ids in model_values.items():
|
||||
if '%' in ref_ids:
|
||||
if domain[1] == 'in':
|
||||
new_domain.append(
|
||||
[reference + '.id', '!=', None, model])
|
||||
else:
|
||||
new_domain.append(
|
||||
[reference, 'not like', model + ',%'])
|
||||
else:
|
||||
new_domain.append(
|
||||
[reference + '.id', domain[1], ref_ids, model])
|
||||
return new_domain
|
||||
return []
|
||||
return domain
|
||||
else:
|
||||
return [prepare_reference_domain(d, reference) for d in domain]
|
||||
|
||||
|
||||
def extract_reference_models(domain, field_name):
|
||||
"returns the set of the models available for field_name"
|
||||
if domain in ('AND', 'OR'):
|
||||
return set()
|
||||
elif is_leaf(domain):
|
||||
local_part = domain[0].split('.', 1)[0]
|
||||
if local_part == field_name and len(domain) > 3:
|
||||
return {domain[3]}
|
||||
return set()
|
||||
else:
|
||||
return reduce(operator.or_,
|
||||
(extract_reference_models(d, field_name) for d in domain))
|
||||
|
||||
|
||||
def eval_domain(domain, context, boolop=operator.and_):
|
||||
"compute domain boolean value according to the context"
|
||||
if is_leaf(domain):
|
||||
return eval_leaf(domain, context, boolop=boolop)
|
||||
elif not domain and boolop is operator.and_:
|
||||
return True
|
||||
elif not domain and boolop is operator.or_:
|
||||
return False
|
||||
elif domain[0] == 'AND':
|
||||
return eval_domain(domain[1:], context)
|
||||
elif domain[0] == 'OR':
|
||||
return eval_domain(domain[1:], context, operator.or_)
|
||||
else:
|
||||
return boolop(bool(eval_domain(domain[0], context)),
|
||||
bool(eval_domain(domain[1:], context, boolop)))
|
||||
|
||||
|
||||
def localize_domain(domain, field_name=None, strip_target=False):
|
||||
"returns only locale part of domain. eg: langage.code -> code"
|
||||
if domain in ('AND', 'OR', True, False):
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
if 'child_of' in domain[1]:
|
||||
if domain[0].count('.'):
|
||||
_, target_part = domain[0].split('.', 1)
|
||||
return [target_part] + list(domain[1:])
|
||||
if len(domain) == 3:
|
||||
return domain
|
||||
else:
|
||||
return [domain[3]] + list(domain[1:-1])
|
||||
locale_name = 'id'
|
||||
if isinstance(domain[2], str):
|
||||
locale_name = 'rec_name'
|
||||
n = 3 if strip_target else 4
|
||||
return [locale_part(domain[0], field_name, locale_name)] \
|
||||
+ list(domain[1:n]) + list(domain[4:])
|
||||
else:
|
||||
return [localize_domain(part, field_name, strip_target)
|
||||
for part in domain]
|
||||
|
||||
|
||||
def _sort_key(domain):
|
||||
if not domain:
|
||||
return (0, tuple())
|
||||
elif is_leaf(domain):
|
||||
return (1, tuple((type(x).__name__, x) for x in domain))
|
||||
elif domain in ['AND', 'OR']:
|
||||
return (0, domain)
|
||||
else:
|
||||
content = tuple(_sort_key(e) for e in domain)
|
||||
nestedness = max(k[0] for k in content)
|
||||
return (nestedness + 1, content)
|
||||
|
||||
|
||||
def sort(domain):
|
||||
"Sort a domain"
|
||||
if not domain:
|
||||
return domain
|
||||
elif is_leaf(domain):
|
||||
return domain
|
||||
elif domain in ['AND', 'OR']:
|
||||
return domain
|
||||
else:
|
||||
return sorted((sort(e) for e in domain), key=_sort_key)
|
||||
|
||||
|
||||
def bool_operator(domain):
|
||||
"Returns the boolean operator used by a domain"
|
||||
bool_op = 'AND'
|
||||
if domain and domain[0] in ['AND', 'OR']:
|
||||
bool_op = domain[0]
|
||||
return bool_op
|
||||
|
||||
|
||||
def simplify_nested(domain):
|
||||
"""Simplify extra domain markers"""
|
||||
if not domain:
|
||||
return []
|
||||
elif is_leaf(domain):
|
||||
return [domain]
|
||||
elif domain in ['OR', 'AND']:
|
||||
return [domain]
|
||||
elif isinstance(domain, list) and len(domain) == 1:
|
||||
return simplify_nested(domain[0])
|
||||
else:
|
||||
simplified = []
|
||||
domain_op = bool_operator(domain)
|
||||
for branch in domain:
|
||||
simplified_branch = simplify_nested(branch)
|
||||
if (bool_operator(simplified_branch) == domain_op
|
||||
or len(simplified_branch) == 1):
|
||||
if (simplified
|
||||
and simplified_branch
|
||||
and simplified_branch[0] in ['AND', 'OR']):
|
||||
simplified.extend(simplified_branch[1:])
|
||||
else:
|
||||
simplified.extend(simplified_branch)
|
||||
else:
|
||||
simplified.append(simplified_branch)
|
||||
return simplified
|
||||
|
||||
|
||||
def simplify_duplicate(domain):
|
||||
"""Remove duplicates subdomain from domain"""
|
||||
dedup_branches = []
|
||||
bool_op = None
|
||||
if domain[0] in ['AND', 'OR']:
|
||||
bool_op, *domain = domain
|
||||
for branch in domain:
|
||||
simplified_branch = simplify(branch)
|
||||
if not simplified_branch:
|
||||
if bool_op == 'OR':
|
||||
return []
|
||||
else:
|
||||
continue
|
||||
elif simplified_branch not in dedup_branches:
|
||||
dedup_branches.append(simplified_branch)
|
||||
if bool_op and len(dedup_branches) > 1:
|
||||
dedup_branches.insert(0, bool_op)
|
||||
return dedup_branches
|
||||
|
||||
|
||||
def simplify_AND(domain):
|
||||
"""Remove useless ANDs"""
|
||||
if is_leaf(domain):
|
||||
return domain
|
||||
elif domain == 'OR':
|
||||
return domain
|
||||
else:
|
||||
return [simplify_AND(e) for e in domain if e != 'AND']
|
||||
|
||||
|
||||
def simplify(domain):
|
||||
"""Remove duplicate expressions and useless OR/AND"""
|
||||
if is_leaf(domain):
|
||||
return [domain]
|
||||
elif not domain:
|
||||
return domain
|
||||
else:
|
||||
return simplify_nested(simplify_duplicate(domain))
|
||||
|
||||
|
||||
def canonicalize(domain):
|
||||
"""Returns the canonical version of a domain.
|
||||
|
||||
The canonical version of a domain is one where the domain is both
|
||||
simplified and sorted
|
||||
"""
|
||||
return simplify_AND(sort(simplify(domain)))
|
||||
|
||||
|
||||
def merge(domain, domoperator=None):
|
||||
if not domain or domain in ('AND', 'OR'):
|
||||
return []
|
||||
domain_type = 'OR' if domain[0] == 'OR' else 'AND'
|
||||
if is_leaf(domain):
|
||||
return [domain]
|
||||
elif domoperator is None:
|
||||
return [domain_type] + reduce(operator.add,
|
||||
[merge(e, domain_type) for e in domain])
|
||||
elif domain_type == domoperator:
|
||||
return reduce(operator.add, [merge(e, domain_type) for e in domain])
|
||||
else:
|
||||
# without setting the domoperator
|
||||
return [merge(domain)]
|
||||
|
||||
|
||||
def concat(*domains, **kwargs):
|
||||
domoperator = kwargs.get('domoperator')
|
||||
result = []
|
||||
if domoperator:
|
||||
result.append(domoperator)
|
||||
for domain in domains:
|
||||
if domain:
|
||||
result.append(domain)
|
||||
return simplify(merge(result))
|
||||
|
||||
|
||||
def unique_value(domain, single_value=True):
|
||||
"Return if unique, the field and the value"
|
||||
if (isinstance(domain, list)
|
||||
and len(domain) == 1):
|
||||
name, operator, value, *model = domain[0]
|
||||
count = name.count('.')
|
||||
if (
|
||||
(operator == '='
|
||||
or (single_value
|
||||
and operator == 'in' and len(value) == 1))
|
||||
and (not count
|
||||
or (count == 1 and model and name.endswith('.id')))):
|
||||
value = value if operator == '=' and single_value else value[0]
|
||||
if model and name.endswith('.id'):
|
||||
model = model[0]
|
||||
value = [model, value]
|
||||
return True, name, value
|
||||
return False, None, None
|
||||
|
||||
|
||||
def parse(domain):
|
||||
if is_leaf(domain):
|
||||
return domain
|
||||
elif not domain:
|
||||
return And([])
|
||||
elif domain[0] == 'OR':
|
||||
return Or(domain[1:])
|
||||
else:
|
||||
return And(domain[1:] if domain[0] == 'AND' else domain)
|
||||
|
||||
|
||||
def domain_inversion(domain, symbol, context=None):
|
||||
"""compute an inversion of the domain eventually the context is used to
|
||||
simplify the expression"""
|
||||
if context is None:
|
||||
context = {}
|
||||
expression = parse(domain)
|
||||
if symbol not in expression.variables:
|
||||
return True
|
||||
return expression.inverse(symbol, context)
|
||||
|
||||
|
||||
class And(object):
|
||||
|
||||
def __init__(self, expressions):
|
||||
self.branches = list(map(parse, expressions))
|
||||
self.variables = set()
|
||||
for expression in self.branches:
|
||||
if is_leaf(expression):
|
||||
self.variables.add(self.base(expression[0]))
|
||||
elif isinstance(expression, And):
|
||||
self.variables |= expression.variables
|
||||
|
||||
def base(self, expression):
|
||||
if '.' not in expression:
|
||||
return expression
|
||||
else:
|
||||
return expression.split('.')[0]
|
||||
|
||||
def inverse(self, symbol, context):
|
||||
result = []
|
||||
for part in self.branches:
|
||||
if isinstance(part, And):
|
||||
part_inversion = part.inverse(symbol, context)
|
||||
evaluated = isinstance(part_inversion, bool)
|
||||
if symbol not in part.variables:
|
||||
continue
|
||||
if not evaluated:
|
||||
result.append(part_inversion)
|
||||
elif part_inversion:
|
||||
continue
|
||||
else:
|
||||
return False
|
||||
elif is_leaf(part) and self.base(part[0]) == symbol:
|
||||
result.append(part)
|
||||
else:
|
||||
field = part[0]
|
||||
if (field not in context
|
||||
or field in context
|
||||
and (eval_leaf(part, context, operator.and_)
|
||||
or constrained_leaf(part, operator.and_))):
|
||||
result.append(True)
|
||||
else:
|
||||
return False
|
||||
|
||||
result = [e for e in result if e is not True]
|
||||
if result == []:
|
||||
return True
|
||||
else:
|
||||
return simplify(result)
|
||||
|
||||
|
||||
class Or(And):
|
||||
|
||||
def inverse(self, symbol, context):
|
||||
result = []
|
||||
known_variables = set(context.keys())
|
||||
if not known_variables >= (self.variables - {symbol}):
|
||||
# In this case we don't know enough about this OR part, we
|
||||
# consider it to be True (because people will have the constraint
|
||||
# on this part later).
|
||||
return True
|
||||
for part in self.branches:
|
||||
if isinstance(part, And):
|
||||
part_inversion = part.inverse(symbol, context)
|
||||
evaluated = isinstance(part_inversion, bool)
|
||||
if symbol not in part.variables:
|
||||
if evaluated and part_inversion:
|
||||
return True
|
||||
continue
|
||||
if not evaluated:
|
||||
result.append(part_inversion)
|
||||
elif part_inversion:
|
||||
return True
|
||||
else:
|
||||
continue
|
||||
elif is_leaf(part) and self.base(part[0]) == symbol:
|
||||
result.append(part)
|
||||
else:
|
||||
field = part[0]
|
||||
field = self.base(field)
|
||||
if (field in context
|
||||
and (eval_leaf(part, context, operator.or_)
|
||||
or constrained_leaf(part, operator.or_))):
|
||||
return True
|
||||
elif (field in context
|
||||
and not eval_leaf(part, context, operator.or_)):
|
||||
result.append(False)
|
||||
|
||||
result = [e for e in result if e is not False]
|
||||
if result == []:
|
||||
return False
|
||||
else:
|
||||
return simplify(['OR'] + result)
|
||||
121
tools/email_.py
Executable file
121
tools/email_.py
Executable file
@@ -0,0 +1,121 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
import re
|
||||
from email.charset import Charset
|
||||
from email.utils import formataddr, parseaddr
|
||||
|
||||
from trytond.pool import Pool
|
||||
|
||||
__all__ = [
|
||||
'set_from_header', 'validate_email', 'normalize_email',
|
||||
'convert_ascii_email', 'EmailNotValidError']
|
||||
|
||||
|
||||
def _domainaddr(address):
|
||||
_, email = parseaddr(address)
|
||||
if '@' in email:
|
||||
return email.split('@', 1)[1]
|
||||
|
||||
|
||||
def set_from_header(message, sender, from_):
|
||||
"Fill email headers to appear at best from the address"
|
||||
if parseaddr(sender)[1] != parseaddr(from_)[1]:
|
||||
if _domainaddr(sender) == _domainaddr(from_):
|
||||
message['From'] = from_
|
||||
message['Sender'] = sender
|
||||
else:
|
||||
message['From'] = sender
|
||||
message['On-Behalf-Of'] = from_
|
||||
message['Reply-To'] = from_
|
||||
else:
|
||||
message['From'] = from_
|
||||
|
||||
|
||||
def has_rcpt(msg):
|
||||
return any((msg['To'], msg['Cc'], msg['Bcc']))
|
||||
|
||||
|
||||
try:
|
||||
from dns.exception import DNSException
|
||||
from email_validator import EmailNotValidError, caching_resolver
|
||||
from email_validator import validate_email as _validate_email
|
||||
|
||||
try:
|
||||
resolver = caching_resolver()
|
||||
except DNSException:
|
||||
if Pool.test:
|
||||
resolver = None
|
||||
else:
|
||||
raise
|
||||
|
||||
def validate_email(email):
|
||||
emailinfo = _validate_email(
|
||||
email, check_deliverability=True,
|
||||
dns_resolver=resolver,
|
||||
test_environment=Pool.test)
|
||||
return emailinfo.normalized
|
||||
|
||||
def normalize_email(email):
|
||||
try:
|
||||
emailinfo = _validate_email(
|
||||
email, check_deliverability=False,
|
||||
test_environment=Pool.test)
|
||||
return emailinfo.normalized
|
||||
except EmailNotValidError:
|
||||
return email
|
||||
|
||||
def convert_ascii_email(email):
|
||||
try:
|
||||
emailinfo = _validate_email(
|
||||
email, check_deliverability=False,
|
||||
test_environment=Pool.test)
|
||||
return emailinfo.ascii_email or emailinfo.normalized
|
||||
except EmailNotValidError:
|
||||
return email
|
||||
|
||||
except ImportError:
|
||||
|
||||
def validate_email(email):
|
||||
return email
|
||||
|
||||
def normalize_email(email):
|
||||
return email
|
||||
|
||||
def convert_ascii_email(email):
|
||||
return email
|
||||
|
||||
class EmailNotValidError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Copy of email.utils.formataddr but without the ASCII enforcement
|
||||
specialsre = re.compile(r'[][\\()<>@,:;".]')
|
||||
escapesre = re.compile(r'[\\"]')
|
||||
|
||||
|
||||
def _formataddr(pair, charset='utf-8'):
|
||||
name, address = pair
|
||||
if name:
|
||||
try:
|
||||
name.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
if isinstance(charset, str):
|
||||
charset = Charset(charset)
|
||||
encoded_name = charset.header_encode(name)
|
||||
return "%s <%s>" % (encoded_name, address)
|
||||
else:
|
||||
quotes = ''
|
||||
if specialsre.search(name):
|
||||
quotes = '"'
|
||||
name = escapesre.sub(r'\\\g<0>', name)
|
||||
return '%s%s%s <%s>' % (quotes, name, quotes, address)
|
||||
return address
|
||||
|
||||
|
||||
def format_address(email, name=None):
|
||||
pair = (name, convert_ascii_email(email))
|
||||
try:
|
||||
return formataddr(pair)
|
||||
except UnicodeEncodeError:
|
||||
return _formataddr(pair)
|
||||
11
tools/gevent.py
Executable file
11
tools/gevent.py
Executable file
@@ -0,0 +1,11 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
|
||||
def is_gevent_monkey_patched():
|
||||
try:
|
||||
from gevent import monkey
|
||||
except ImportError:
|
||||
return False
|
||||
else:
|
||||
return monkey.is_module_patched('__builtin__')
|
||||
26
tools/immutabledict.py
Executable file
26
tools/immutabledict.py
Executable file
@@ -0,0 +1,26 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
|
||||
# repository contains the full copyright notices and license terms.
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
class ImmutableDict(dict):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def _not_allowed(cls, *args, **kwargs):
|
||||
raise TypeError("Operation not allowed on ImmutableDict")
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return ImmutableDict(
|
||||
(k, deepcopy(v, memo=memo)) for k, v in self.items())
|
||||
|
||||
__setitem__ = _not_allowed
|
||||
__delitem__ = _not_allowed
|
||||
__ior__ = _not_allowed
|
||||
clear = _not_allowed
|
||||
pop = _not_allowed
|
||||
popitem = _not_allowed
|
||||
setdefault = _not_allowed
|
||||
update = _not_allowed
|
||||
|
||||
del _not_allowed
|
||||
126
tools/logging.py
Executable file
126
tools/logging.py
Executable file
@@ -0,0 +1,126 @@
|
||||
from collections.abc import Iterable, Mapping
|
||||
from itertools import islice
|
||||
|
||||
_MAX_ARGUMENTS = 5
|
||||
_MAX_ITEMS = 5
|
||||
_ELLIPSE = '...'
|
||||
_MAX_STR_LENGTH = 20 - len(_ELLIPSE)
|
||||
|
||||
|
||||
class Ellipse:
|
||||
|
||||
__slots__ = ('text',)
|
||||
|
||||
def __init__(self, text=_ELLIPSE):
|
||||
self.text = text
|
||||
|
||||
def __str__(self):
|
||||
return self.text
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class EllipseDict:
|
||||
|
||||
__slots__ = ('items',)
|
||||
|
||||
def __init__(self, items):
|
||||
self.items = items
|
||||
|
||||
def __str__(self):
|
||||
strings = []
|
||||
ellipse = False
|
||||
for key, value in self.items:
|
||||
if isinstance(key, Ellipse):
|
||||
ellipse = True
|
||||
break
|
||||
strings.append(f'{key!r}: {value!r}')
|
||||
if ellipse:
|
||||
strings.append(_ELLIPSE)
|
||||
|
||||
s = ', '.join(strings)
|
||||
return '{' + s + '}'
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class format_args:
|
||||
|
||||
__slots__ = ('args', 'kwargs', 'verbose', 'max_args', 'max_items')
|
||||
|
||||
def __init__(self, args, kwargs, verbose=False,
|
||||
max_args=_MAX_ARGUMENTS, max_items=_MAX_ITEMS):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.verbose = verbose
|
||||
self.max_args = max_args
|
||||
self.max_items = max_items
|
||||
|
||||
def __str__(self):
|
||||
_nb_args = self.max_args
|
||||
_nb_items = self.max_items
|
||||
|
||||
def _shorten_sequence(value):
|
||||
nonlocal _nb_items
|
||||
|
||||
for v in islice(value, None, self.max_items + 1):
|
||||
if not self.verbose and not _nb_items:
|
||||
yield Ellipse()
|
||||
break
|
||||
yield v
|
||||
_nb_items -= 1
|
||||
|
||||
def _log_repr(value):
|
||||
if self.verbose:
|
||||
return value
|
||||
elif isinstance(value, bytes):
|
||||
return Ellipse(f'<{len(value)} bytes>')
|
||||
elif isinstance(value, str):
|
||||
if len(value) <= _MAX_STR_LENGTH:
|
||||
return value
|
||||
return (value[:_MAX_STR_LENGTH]
|
||||
+ (_ELLIPSE if len(value) > _MAX_STR_LENGTH else ''))
|
||||
elif isinstance(value, Mapping):
|
||||
def shorten(value):
|
||||
for items in _shorten_sequence(value.items()):
|
||||
if isinstance(items, Ellipse):
|
||||
yield Ellipse(), Ellipse()
|
||||
break
|
||||
key, value = items
|
||||
yield _log_repr(key), _log_repr(value)
|
||||
|
||||
return EllipseDict(shorten(value))
|
||||
elif isinstance(value, Iterable):
|
||||
return type(value)(_log_repr(v)
|
||||
for v in _shorten_sequence(value))
|
||||
else:
|
||||
return value
|
||||
|
||||
s = '('
|
||||
|
||||
logged_args = []
|
||||
for args in self.args:
|
||||
if not _nb_args and not self.verbose:
|
||||
logged_args.append(Ellipse())
|
||||
break
|
||||
_nb_items = self.max_items
|
||||
logged_args.append(_log_repr(args))
|
||||
_nb_args -= 1
|
||||
s += ', '.join(repr(a) for a in logged_args)
|
||||
|
||||
if self.kwargs and (not logged_args
|
||||
or not isinstance(logged_args[-1], Ellipse)):
|
||||
s += ', ' if self.args and self.kwargs else ''
|
||||
logged_kwargs = []
|
||||
for key, value in self.kwargs.items():
|
||||
if not _nb_args and not self.verbose:
|
||||
logged_kwargs.append(repr(Ellipse()))
|
||||
break
|
||||
_nb_items = self.max_items
|
||||
logged_kwargs.append(
|
||||
f'{_log_repr(key)}={_log_repr(value)!r}')
|
||||
_nb_args -= 1
|
||||
s += ', '.join(logged_kwargs)
|
||||
|
||||
s += ')'
|
||||
return s
|
||||
324
tools/misc.py
Executable file
324
tools/misc.py
Executable file
@@ -0,0 +1,324 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
"""
|
||||
Miscelleanous tools used by tryton
|
||||
"""
|
||||
import importlib
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import types
|
||||
import unicodedata
|
||||
import warnings
|
||||
from array import array
|
||||
from collections.abc import Iterable, Sized
|
||||
from functools import wraps
|
||||
from itertools import chain, islice, tee, zip_longest
|
||||
|
||||
from sql import Literal
|
||||
from sql.conditionals import Case
|
||||
from sql.operators import Or
|
||||
|
||||
from trytond.const import MODULES_GROUP, OPERATORS
|
||||
|
||||
try:
|
||||
from backports.entry_points_selectable import entry_points as _entry_points
|
||||
except ImportError:
|
||||
from importlib.metadata import entry_points as _entry_points
|
||||
_ENTRY_POINTS = None
|
||||
|
||||
|
||||
def entry_points():
|
||||
global _ENTRY_POINTS
|
||||
if _ENTRY_POINTS is None:
|
||||
_ENTRY_POINTS = _entry_points()
|
||||
return _ENTRY_POINTS
|
||||
|
||||
|
||||
def import_module(name):
|
||||
try:
|
||||
ep, = entry_points().select(group=MODULES_GROUP, name=name)
|
||||
except ValueError:
|
||||
return importlib.import_module(f'{MODULES_GROUP}.{name}')
|
||||
return ep.load()
|
||||
|
||||
|
||||
def file_open(name, mode="r", subdir='modules', encoding=None):
|
||||
"Open a file from the root directory, using subdir folder"
|
||||
path = find_path(name, subdir, _test=None)
|
||||
return io.open(path, mode, encoding=encoding)
|
||||
|
||||
|
||||
def find_path(name, subdir='modules', _test=os.path.isfile):
|
||||
"Return path from the root directory, using subdir folder"
|
||||
root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def secure_join(root, *paths):
|
||||
"Join paths and ensure it still below root"
|
||||
path = os.path.join(root, *paths)
|
||||
path = os.path.normpath(path)
|
||||
if not path.startswith(os.path.join(root, '')):
|
||||
raise IOError("Permission denied: %s" % name)
|
||||
return path
|
||||
|
||||
if subdir:
|
||||
if subdir == 'modules':
|
||||
try:
|
||||
module_name, module_path = name.split(os.sep, 1)
|
||||
except ValueError:
|
||||
module_name, module_path = name, ''
|
||||
if module_name in {'ir', 'res', 'tests'}:
|
||||
path = secure_join(root_path, module_name, module_path)
|
||||
else:
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
except ModuleNotFoundError:
|
||||
path = secure_join(root_path, subdir, name)
|
||||
else:
|
||||
path = os.path.dirname(module.__file__)
|
||||
if module_path:
|
||||
path = secure_join(path, module_path)
|
||||
else:
|
||||
path = secure_join(root_path, subdir, name)
|
||||
else:
|
||||
path = secure_join(root_path, name)
|
||||
|
||||
if not _test or _test(path):
|
||||
return path
|
||||
else:
|
||||
raise FileNotFoundError("No such file or directory: %r" % name)
|
||||
|
||||
|
||||
def find_dir(name, subdir='modules'):
|
||||
"Return directory from the root directory, using subdir folder"
|
||||
return find_path(name, subdir=subdir, _test=os.path.isdir)
|
||||
|
||||
|
||||
def get_smtp_server():
|
||||
"""
|
||||
Instanciate, configure and return a SMTP or SMTP_SSL instance from
|
||||
smtplib.
|
||||
:return: A SMTP instance. The quit() method must be call when all
|
||||
the calls to sendmail() have been made.
|
||||
"""
|
||||
from trytond.sendmail import get_smtp_server
|
||||
warnings.warn(
|
||||
'get_smtp_server is deprecated use trytond.sendmail',
|
||||
DeprecationWarning)
|
||||
return get_smtp_server()
|
||||
|
||||
|
||||
def reduce_ids(field, ids):
|
||||
'''
|
||||
Return a small SQL expression for the list of ids and the sql column
|
||||
'''
|
||||
if __debug__:
|
||||
def strict_int(value):
|
||||
assert not isinstance(value, float) or value.is_integer(), \
|
||||
"ids must be integer"
|
||||
return int(value)
|
||||
else:
|
||||
strict_int = int
|
||||
ids = list(map(strict_int, ids))
|
||||
if not ids:
|
||||
return Literal(False)
|
||||
ids.sort()
|
||||
prev = ids.pop(0)
|
||||
continue_list = [prev, prev]
|
||||
discontinue_list = array('l')
|
||||
sql = Or()
|
||||
for i in ids:
|
||||
if i == prev:
|
||||
continue
|
||||
if i != prev + 1:
|
||||
if continue_list[-1] - continue_list[0] < 5:
|
||||
discontinue_list.extend([continue_list[0] + x for x in
|
||||
range(continue_list[-1] - continue_list[0] + 1)])
|
||||
else:
|
||||
sql.append((field >= continue_list[0])
|
||||
& (field <= continue_list[-1]))
|
||||
continue_list = []
|
||||
continue_list.append(i)
|
||||
prev = i
|
||||
if continue_list[-1] - continue_list[0] < 5:
|
||||
discontinue_list.extend([continue_list[0] + x for x in
|
||||
range(continue_list[-1] - continue_list[0] + 1)])
|
||||
else:
|
||||
sql.append((field >= continue_list[0]) & (field <= continue_list[-1]))
|
||||
if discontinue_list:
|
||||
sql.append(field.in_(discontinue_list))
|
||||
return sql
|
||||
|
||||
|
||||
def reduce_domain(domain):
|
||||
'''
|
||||
Reduce domain
|
||||
'''
|
||||
if not domain:
|
||||
return []
|
||||
operator = 'AND'
|
||||
if isinstance(domain[0], str):
|
||||
operator = domain[0]
|
||||
domain = domain[1:]
|
||||
result = [operator]
|
||||
for arg in domain:
|
||||
if (isinstance(arg, tuple)
|
||||
or (isinstance(arg, list)
|
||||
and len(arg) > 2
|
||||
and arg[1] in OPERATORS)):
|
||||
# clause
|
||||
result.append(arg)
|
||||
elif isinstance(arg, list) and arg:
|
||||
# sub-domain
|
||||
sub_domain = reduce_domain(arg)
|
||||
sub_operator = sub_domain[0]
|
||||
if sub_operator == operator:
|
||||
result.extend(sub_domain[1:])
|
||||
else:
|
||||
result.append(sub_domain)
|
||||
else:
|
||||
result.append(arg)
|
||||
return result
|
||||
|
||||
|
||||
def grouped_slice(records, count=None):
|
||||
'Grouped slice'
|
||||
from trytond.transaction import Transaction
|
||||
if count is None:
|
||||
count = Transaction().database.IN_MAX
|
||||
count = max(1, count)
|
||||
if not isinstance(records, Sized):
|
||||
records = list(records)
|
||||
for i in range(0, len(records), count):
|
||||
yield islice(records, i, i + count)
|
||||
|
||||
|
||||
def pairwise_longest(iterable):
|
||||
a, b = tee(iterable)
|
||||
next(b, None)
|
||||
return zip_longest(a, b)
|
||||
|
||||
|
||||
def is_instance_method(cls, method):
|
||||
for klass in cls.__mro__:
|
||||
type_ = klass.__dict__.get(method)
|
||||
if type_ is not None:
|
||||
return isinstance(type_, types.FunctionType)
|
||||
|
||||
|
||||
def resolve(name):
|
||||
"Resolve a dotted name to a global object."
|
||||
name = name.split('.')
|
||||
used = name.pop(0)
|
||||
found = importlib.import_module(used)
|
||||
for n in name:
|
||||
used = used + '.' + n
|
||||
try:
|
||||
found = getattr(found, n)
|
||||
except AttributeError:
|
||||
found = importlib.import_module(used)
|
||||
return found
|
||||
|
||||
|
||||
def strip_wildcard(string, wildcard='%', escape='\\'):
|
||||
"Strip starting and ending wildcard from string"
|
||||
string = lstrip_wildcard(string, wildcard)
|
||||
return rstrip_wildcard(string, wildcard, escape)
|
||||
|
||||
|
||||
def lstrip_wildcard(string, wildcard='%'):
|
||||
"Strip starting wildcard from string"
|
||||
if string and string.startswith(wildcard):
|
||||
string = string[1:]
|
||||
return string
|
||||
|
||||
|
||||
def rstrip_wildcard(string, wildcard='%', escape='\\'):
|
||||
"Strip ending wildcard from string"
|
||||
if (string
|
||||
and string.endswith(wildcard)
|
||||
and not string.endswith(escape + wildcard)):
|
||||
string = string[:-1]
|
||||
return string
|
||||
|
||||
|
||||
def escape_wildcard(string, wildcards='%_', escape='\\'):
|
||||
for wildcard in escape + wildcards:
|
||||
string = string.replace(wildcard, escape + wildcard)
|
||||
return string
|
||||
|
||||
|
||||
def unescape_wildcard(string, wildcards='%_', escape='\\'):
|
||||
for wildcard in wildcards + escape:
|
||||
string = string.replace(escape + wildcard, wildcard)
|
||||
return string
|
||||
|
||||
|
||||
def is_full_text(value, escape='\\'):
|
||||
escaped = strip_wildcard(value, escape=escape)
|
||||
escaped = escaped.replace(escape + '%', '').replace(escape + '_', '')
|
||||
if '%' in escaped or '_' in escaped:
|
||||
return False
|
||||
return value.startswith('%') == value.endswith('%')
|
||||
|
||||
|
||||
def likify(string, escape='\\'):
|
||||
if not string:
|
||||
return '%'
|
||||
escaped = string.replace(escape + '%', '').replace(escape + '_', '')
|
||||
if '%' in escaped or '_' in escaped:
|
||||
return string
|
||||
else:
|
||||
return '%' + string + '%'
|
||||
|
||||
|
||||
_slugify_strip_re = re.compile(r'[^\w\s-]')
|
||||
_slugify_hyphenate_re = re.compile(r'[-\s]+')
|
||||
|
||||
|
||||
def slugify(value, hyphenate='-'):
|
||||
if not isinstance(value, str):
|
||||
value = str(value)
|
||||
value = unicodedata.normalize('NFKD', value)
|
||||
value = str(_slugify_strip_re.sub('', value).strip())
|
||||
return _slugify_hyphenate_re.sub(hyphenate, value)
|
||||
|
||||
|
||||
def sortable_values(func):
|
||||
"Decorator that makes list of values sortable"
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
result = list(func(*args, **kwargs))
|
||||
for i, value in enumerate(list(result)):
|
||||
if not isinstance(value, Iterable):
|
||||
value = [value]
|
||||
result[i] = tuple(chain((k is None, k) for k in value))
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
|
||||
def sql_pairing(x, y):
|
||||
"""Return SQL expression to pair x and y
|
||||
Pairing function from http://szudzik.com/ElegantPairing.pdf"""
|
||||
return Case(
|
||||
(x < y, (y * y) + x),
|
||||
else_=(x * x) + x + y)
|
||||
|
||||
|
||||
def firstline(text):
|
||||
"Returns first non-empty line"
|
||||
try:
|
||||
return next((x for x in text.splitlines() if x.strip()))
|
||||
except StopIteration:
|
||||
return ''
|
||||
|
||||
|
||||
def remove_forbidden_chars(value):
|
||||
from trytond.model.fields import Char
|
||||
if value is None:
|
||||
return value
|
||||
for c in Char.forbidden_chars:
|
||||
if c in value:
|
||||
value = value.replace(c, ' ')
|
||||
return value.strip()
|
||||
52
tools/qrcode.py
Executable file
52
tools/qrcode.py
Executable file
@@ -0,0 +1,52 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import qrcode
|
||||
from qrcode.image.pil import PilImage
|
||||
from qrcode.image.svg import SvgImage
|
||||
from webcolors import name_to_rgb
|
||||
|
||||
__all__ = ['generate_svg', 'generate_png']
|
||||
|
||||
|
||||
def _generate(
|
||||
code, image_factory, error_correction, box_size, border, **options):
|
||||
output = BytesIO()
|
||||
qr = qrcode.QRCode(
|
||||
image_factory=image_factory,
|
||||
error_correction=error_correction,
|
||||
box_size=box_size, border=border)
|
||||
qr.add_data(code)
|
||||
qr.make_image(**options).save(output)
|
||||
return output
|
||||
|
||||
|
||||
def _error_correction(value):
|
||||
return getattr(qrcode, f'ERROR_CORRECT_{value.upper()}')
|
||||
|
||||
|
||||
def generate_svg(
|
||||
code, box_size=10, border=4, error_correction='M',
|
||||
background='white', foreground='black'):
|
||||
|
||||
class FactoryImage(SvgImage):
|
||||
pass
|
||||
setattr(FactoryImage, 'background', background)
|
||||
return _generate(
|
||||
code, box_size=box_size, border=border,
|
||||
error_correction=_error_correction(error_correction),
|
||||
image_factory=SvgImage)
|
||||
|
||||
|
||||
def generate_png(
|
||||
code, box_size=10, border=4, error_correction='M',
|
||||
background='white', foreground='black'):
|
||||
background = name_to_rgb(background)
|
||||
foreground = name_to_rgb(foreground)
|
||||
return _generate(
|
||||
code, box_size=box_size, border=border,
|
||||
error_correction=_error_correction(error_correction),
|
||||
back_color=background, fill_color=foreground,
|
||||
image_factory=PilImage)
|
||||
16
tools/singleton.py
Executable file
16
tools/singleton.py
Executable file
@@ -0,0 +1,16 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
'''
|
||||
Metaclass for singleton pattern
|
||||
'''
|
||||
def __init__(cls, name, bases, dict):
|
||||
super(Singleton, cls).__init__(name, bases, dict)
|
||||
cls.instance = None
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls.instance is None:
|
||||
cls.instance = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls.instance
|
||||
123
tools/string_.py
Executable file
123
tools/string_.py
Executable file
@@ -0,0 +1,123 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
# Code come from python-Levenshtein
|
||||
|
||||
__all__ = ['StringMatcher', 'StringPartitioned', 'LazyString']
|
||||
|
||||
from warnings import warn
|
||||
|
||||
try:
|
||||
from Levenshtein import distance, editops, matching_blocks, opcodes, ratio
|
||||
|
||||
class StringMatcher:
|
||||
"""A SequenceMatcher-like class built on the top of Levenshtein"""
|
||||
|
||||
def _reset_cache(self):
|
||||
self._ratio = self._distance = None
|
||||
self._opcodes = self._editops = self._matching_blocks = None
|
||||
|
||||
def __init__(self, isjunk=None, seq1='', seq2=''):
|
||||
if isjunk:
|
||||
warn("isjunk not NOT implemented, it will be ignored")
|
||||
self._str1, self._str2 = seq1, seq2
|
||||
self._reset_cache()
|
||||
|
||||
def set_seqs(self, seq1, seq2):
|
||||
self._str1, self._str2 = seq1, seq2
|
||||
self._reset_cache()
|
||||
|
||||
def set_seq1(self, seq1):
|
||||
self._str1 = seq1
|
||||
self._reset_cache()
|
||||
|
||||
def set_seq2(self, seq2):
|
||||
self._str2 = seq2
|
||||
self._reset_cache()
|
||||
|
||||
def get_opcodes(self):
|
||||
if not self._opcodes:
|
||||
if self._editops:
|
||||
self._opcodes = opcodes(
|
||||
self._editops, self._str1, self._str2)
|
||||
else:
|
||||
self._opcodes = opcodes(self._str1, self._str2)
|
||||
return self._opcodes
|
||||
|
||||
def get_editops(self):
|
||||
if not self._editops:
|
||||
if self._opcodes:
|
||||
self._editops = editops(
|
||||
self._opcodes, self._str1, self._str2)
|
||||
else:
|
||||
self._editops = editops(self._str1, self._str2)
|
||||
return self._editops
|
||||
|
||||
def get_matching_blocks(self):
|
||||
if not self._matching_blocks:
|
||||
self._matching_blocks = matching_blocks(self.get_opcodes(),
|
||||
self._str1, self._str2)
|
||||
return self._matching_blocks
|
||||
|
||||
def ratio(self):
|
||||
if not self._ratio:
|
||||
self._ratio = ratio(self._str1, self._str2)
|
||||
return self._ratio
|
||||
|
||||
def quick_ratio(self):
|
||||
# This is usually quick enough :o)
|
||||
if not self._ratio:
|
||||
self._ratio = ratio(self._str1, self._str2)
|
||||
return self._ratio
|
||||
|
||||
def real_quick_ratio(self):
|
||||
len1, len2 = len(self._str1), len(self._str2)
|
||||
return 2.0 * min(len1, len2) / (len1 + len2)
|
||||
|
||||
def distance(self):
|
||||
if not self._distance:
|
||||
self._distance = distance(self._str1, self._str2)
|
||||
return self._distance
|
||||
except ImportError:
|
||||
from difflib import SequenceMatcher as StringMatcher
|
||||
|
||||
|
||||
class StringPartitioned(str):
|
||||
"A string subclass that stores parts that composes itself."
|
||||
__slots__ = ('_parts',)
|
||||
|
||||
def __init__(self, base):
|
||||
super().__init__()
|
||||
if isinstance(base, StringPartitioned):
|
||||
self._parts = base._parts
|
||||
else:
|
||||
self._parts = (base,)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._parts)
|
||||
|
||||
def __add__(self, other):
|
||||
new = self.__class__(str(self) + other)
|
||||
new._parts = self._parts + (other,)
|
||||
return new
|
||||
|
||||
def __radd__(self, other):
|
||||
new = self.__class__(other + str(self))
|
||||
new._parts = (other,) + self._parts
|
||||
return new
|
||||
|
||||
|
||||
class LazyString():
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
self._func = func
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
def __str__(self):
|
||||
return self._func(*self._args, **self._kwargs)
|
||||
|
||||
def __add__(self, other):
|
||||
return str(self) + other
|
||||
|
||||
def __radd__(self, other):
|
||||
return other + str(self)
|
||||
46
tools/timezone.py
Executable file
46
tools/timezone.py
Executable file
@@ -0,0 +1,46 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import logging
|
||||
import os
|
||||
|
||||
try:
|
||||
import zoneinfo
|
||||
ZoneInfo = zoneinfo.ZoneInfo
|
||||
ZoneInfoNotFoundError = zoneinfo.ZoneInfoNotFoundError
|
||||
except ImportError:
|
||||
zoneinfo = None
|
||||
import pytz
|
||||
from dateutil.tz import gettz as ZoneInfo
|
||||
|
||||
class ZoneInfoNotFoundError(KeyError):
|
||||
pass
|
||||
|
||||
__all__ = ['SERVER', 'UTC', 'get_tzinfo', 'available_timezones']
|
||||
logger = logging.getLogger(__name__)
|
||||
_ALL_ZONES = None
|
||||
|
||||
|
||||
def available_timezones():
|
||||
global _ALL_ZONES
|
||||
|
||||
if not _ALL_ZONES:
|
||||
if zoneinfo:
|
||||
_ALL_ZONES = zoneinfo.available_timezones()
|
||||
else:
|
||||
_ALL_ZONES = set(pytz.all_timezones)
|
||||
return set(_ALL_ZONES)
|
||||
|
||||
|
||||
def get_tzinfo(zoneid):
|
||||
try:
|
||||
zi = ZoneInfo(zoneid)
|
||||
if not zi:
|
||||
raise ZoneInfoNotFoundError
|
||||
except ZoneInfoNotFoundError:
|
||||
logger.warning("Timezone %s not found falling back to UTC", zoneid)
|
||||
zi = UTC
|
||||
return zi
|
||||
|
||||
|
||||
UTC = ZoneInfo('UTC')
|
||||
SERVER = get_tzinfo(os.environ['TRYTOND_TZ'])
|
||||
Reference in New Issue
Block a user