Coverage for adhoc-cicd-odoo-odoo / odoo / orm / utils.py: 77%
56 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:05 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:05 +0000
1import re
2import warnings
3from collections.abc import Set as AbstractSet
5import dateutil.relativedelta
7from odoo.exceptions import AccessError, ValidationError
8from odoo.tools import SQL
10regex_alphanumeric = re.compile(r'^[a-z0-9_]+$')
11regex_object_name = re.compile(r'^[a-z0-9_.]+$')
12regex_pg_name = re.compile(r'^[a-z_][a-z0-9_$]*$', re.IGNORECASE)
13# match private methods, to prevent their remote invocation
14regex_private = re.compile(r'^(_.*|init)$')
16# types handled as collections
17COLLECTION_TYPES = (list, tuple, AbstractSet)
18# The hard-coded super-user id (a.k.a. root user, or OdooBot).
19SUPERUSER_ID = 1
21# _read_group stuff
22READ_GROUP_TIME_GRANULARITY = {
23 'hour': dateutil.relativedelta.relativedelta(hours=1),
24 'day': dateutil.relativedelta.relativedelta(days=1),
25 'week': dateutil.relativedelta.relativedelta(days=7),
26 'month': dateutil.relativedelta.relativedelta(months=1),
27 'quarter': dateutil.relativedelta.relativedelta(months=3),
28 'year': dateutil.relativedelta.relativedelta(years=1)
29}
31READ_GROUP_NUMBER_GRANULARITY = {
32 'year_number': 'year',
33 'quarter_number': 'quarter',
34 'month_number': 'month',
35 'iso_week_number': 'week', # ISO week number because anything else than ISO is nonsense
36 'day_of_year': 'doy',
37 'day_of_month': 'day',
38 'day_of_week': 'dow',
39 'hour_number': 'hour',
40 'minute_number': 'minute',
41 'second_number': 'second',
42}
44READ_GROUP_ALL_TIME_GRANULARITY = READ_GROUP_TIME_GRANULARITY | READ_GROUP_NUMBER_GRANULARITY
47# SQL operators with spaces around them
48# hardcoded to avoid changing SQL injection linting
49SQL_OPERATORS = {
50 "=": SQL(" = "),
51 "!=": SQL(" != "),
52 "in": SQL(" IN "),
53 "not in": SQL(" NOT IN "),
54 "<": SQL(" < "),
55 ">": SQL(" > "),
56 "<=": SQL(" <= "),
57 ">=": SQL(" >= "),
58 "like": SQL(" LIKE "),
59 "ilike": SQL(" ILIKE "),
60 "=like": SQL(" LIKE "),
61 "=ilike": SQL(" ILIKE "),
62 "not like": SQL(" NOT LIKE "),
63 "not ilike": SQL(" NOT ILIKE "),
64 "not =like": SQL(" NOT LIKE "),
65 "not =ilike": SQL(" NOT ILIKE "),
66}
69def check_method_name(name):
70 """ Raise an ``AccessError`` if ``name`` is a private method name. """
71 warnings.warn("Since 19.0, use odoo.service.model.get_public_method", DeprecationWarning)
72 if regex_private.match(name):
73 raise AccessError('Private methods (such as %s) cannot be called remotely.' % name)
76def check_object_name(name):
77 """ Check if the given name is a valid model name.
79 The _name attribute in osv and osv_memory object is subject to
80 some restrictions. This function returns True or False whether
81 the given name is allowed or not.
83 TODO: this is an approximation. The goal in this approximation
84 is to disallow uppercase characters (in some places, we quote
85 table/column names and in other not, which leads to this kind
86 of errors:
88 psycopg2.ProgrammingError: relation "xxx" does not exist).
90 The same restriction should apply to both osv and osv_memory
91 objects for consistency.
93 """
94 return regex_object_name.match(name) is not None
97def check_pg_name(name):
98 """ Check whether the given name is a valid PostgreSQL identifier name. """
99 if not regex_pg_name.match(name): 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true
100 raise ValidationError("Invalid characters in table name %r" % name)
101 if len(name) > 63: 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 raise ValidationError("Table name %r is too long" % name)
105def parse_field_expr(field_expr: str) -> tuple[str, str | None]:
106 if (property_index := field_expr.find(".")) >= 0:
107 property_name = field_expr[property_index + 1:]
108 field_expr = field_expr[:property_index]
109 else:
110 property_name = None
111 if not field_expr: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true
112 raise ValueError(f"Invalid field expression {field_expr!r}")
113 return field_expr, property_name
116def expand_ids(id0, ids):
117 """ Return an iterator of unique ids from the concatenation of ``[id0]`` and
118 ``ids``, and of the same kind (all real or all new).
119 """
120 yield id0
121 seen = {id0}
122 kind = bool(id0)
123 for id_ in ids:
124 if id_ not in seen and bool(id_) == kind:
125 yield id_
126 seen.add(id_)
129class OriginIds:
130 """ A reversible iterable returning the origin ids of a collection of ``ids``.
131 Actual ids are returned as is, and ids without origin are not returned.
132 """
133 __slots__ = ['ids']
135 def __init__(self, ids):
136 self.ids = ids
138 def __iter__(self):
139 for id_ in self.ids:
140 if id_ := id_ or getattr(id_, 'origin', None):
141 yield id_
143 def __reversed__(self):
144 for id_ in reversed(self.ids):
145 if id_ := id_ or getattr(id_, 'origin', None):
146 yield id_
149origin_ids = OriginIds