Initial import from Docker volume

This commit is contained in:
root
2025-12-26 13:11:43 +00:00
commit 4998dc066a
13336 changed files with 1767801 additions and 0 deletions

97
tools/__init__.py Executable file
View 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,
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

67
tools/barcode.py Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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'])