mangono-addon-auto_migration 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: mangono-addon-auto_migration
3
+ Version: 2.0.0
4
+ Project-URL: Homepage, https://mangono.fr
5
+ Author-Email: mangono <opensource+odoo@mangono.fr>
6
+ License-Expression: AGPL-3.0
7
+ Classifier: Framework :: Odoo
8
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
9
+ Classifier: Programming Language :: Python
10
+ Requires-Dist: environ-odoo-config<2,>=0.6.5
11
+ Description-Content-Type: text/plain
12
+ Description: This server wide module provides support to run pre- and post-installation scripts
13
+
@@ -0,0 +1,12 @@
1
+ odoo/addons/auto_migration/__init__.py,sha256=XnrwL7vmyhRTYi9YvudtdpYr16kG23j-k59TRAWIQfU,123
2
+ odoo/addons/auto_migration/__manifest__.py,sha256=xLi6VJzYkiJyDSj9yLEnTLEAmWLQqbzXBn_2JOiWe8M,489
3
+ odoo/addons/auto_migration/auto_load.py,sha256=m70OuVD-Is7JA91Tu9wypgghr0ljAt1u3Wx7j-JY1A4,237
4
+ odoo/addons/auto_migration/documentation.adoc,sha256=Hsp3wC5Y4LJTuw01efzzIZKlYrZhL75IQFBd0XFeaoE,6645
5
+ odoo/addons/auto_migration/hooks.py,sha256=i9xOAVzoBtRVyDhs9f6b-wJAboXqwEuT16LIS9345xY,637
6
+ odoo/addons/auto_migration/mangono_migration.py,sha256=BVjtDYW0ohkeVehEDnuHmlAfH-2BNXGFzg4CNJVvnuc,16123
7
+ odoo/addons/auto_migration/i18n/fr.po,sha256=UqeDlp8ksOuoMFCBU9M3xJvqE09s3kKoKpCT2vK9uTA,432
8
+ mangono_addon_auto_migration-2.0.0.dist-info/METADATA,sha256=m7iQ5kAo1-_kb5yhHQ4tb4kwqbVnxSbGZV7a3KEZVr4,522
9
+ mangono_addon_auto_migration-2.0.0.dist-info/WHEEL,sha256=oMFM00-3w15fuhxrOoJBKP3EfQFiV1TFGhhmJn2iH-o,100
10
+ mangono_addon_auto_migration-2.0.0.dist-info/entry_points.txt,sha256=oHx9JvLBZVSkJLv8uLpO0a895NFrUwRB-QzzmvEYSQM,126
11
+ mangono_addon_auto_migration-2.0.0.dist-info/top_level.txt,sha256=QE6RBQ0QX5f4eFuUcGgU5Kbq1A_qJcDs-e_vpr6pmfU,4
12
+ mangono_addon_auto_migration-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: Mangono Wheel Builder: 0.5.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [environ_odoo_config.auto_server_wide_module]
2
+ auto_migration = odoo.addons.auto_migration.auto_load:auto_load_auto_migration
3
+
@@ -0,0 +1,3 @@
1
+ from . import mangono_migration
2
+ from .hooks import create_migration_channel
3
+ from .mangono_migration import patch_migration
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "auto_migration",
3
+ "description": "This server wide module provides support to run pre- and post-installation scripts",
4
+ "category": "Extra Tools",
5
+ "depends": ["base"],
6
+ "installable": False,
7
+ "auto_install": False,
8
+ "version": "2.0.0",
9
+ "author": "mangono",
10
+ "license": "AGPL-3",
11
+ "maintainer": "Mangono",
12
+ "website": "https://mangono.fr",
13
+ "data": [],
14
+ "pre_init_hook": "create_migration_channel",
15
+ "post_load": "patch_migration",
16
+ }
@@ -0,0 +1,4 @@
1
+ def auto_load_mangono_migration(environ: dict) -> bool:
2
+ return (environ.get("UPDATE", False) or environ.get("INIT", False)) and not environ.get(
3
+ "DISABLE_MANGONO_MIGRATION", environ.get("DISABLE_AUTO_MIGRATION", False)
4
+ )
@@ -0,0 +1,159 @@
1
+ = AUTO MIGRATION
2
+
3
+ == Purpose
4
+
5
+ Provide service for pre-installation and post-installation scripts (migration scripts)
6
+
7
+ scripts are run upon install or update (-i ou -u)
8
+ Goal is to avoid having to play manually SQL or python script
9
+
10
+ We do not use odoo migration script system because it requires to manage module's version, which is difficult for project with simultaneous dev
11
+
12
+ == Usage
13
+ === settings
14
+ this module must be declared as server wide module
15
+
16
+ - docker : `--load web,kanban,auto_migration`
17
+ - nestor : using `nestor.cfg` file at the project's root, for example :
18
+ ----
19
+ default:
20
+ version: "13.0"
21
+ workers: 3
22
+ options:
23
+ load: auto_migration
24
+ ----
25
+ - clever cloud : using `.clever/envs/update.env` file
26
+ ----
27
+ LOAD_AUTO_MIGRATION="True"
28
+ ----
29
+
30
+ - odoo-cloud
31
+
32
+ This module is auto-loadable and will be loaded if the library is present in your `requirements.txt`
33
+
34
+ You may need to add an explicit load if you want to use it in the install step of the runbot
35
+ ----
36
+ variables:
37
+ # ODOO_ENV aare propagated to the odoo service inside the runbot
38
+ LOAD_AUTO_MIGRATION: auto_migration
39
+ ----
40
+
41
+ - mise runbot
42
+
43
+ If you need the module loaded in the install step of mise runbot, you need to set `ODOO_ENV_CONFIG_EXCLUDE_EXT: "auto_load_widemodules"` in addition to `LOAD_AUTO_MIGRATION`.
44
+
45
+ `*IMPORTANT*` : *do not install the auto_migration module!* +
46
+
47
+
48
+ `auto_migrations` directory must be created at the root of your module's directory (__`mangono_migration` directory is kept for backward compatibility, will be deprecated in future release__)
49
+
50
+ A `auto_migration` table will be created in your database to record the status of the script running and record logs. *There is no graphical interface, and there is no odoo models*.
51
+ You can use SQL request to check script execution, and use usual `_logger.info()` to log script activity.
52
+
53
+ _auto_migration library is available from V12 to V19, with python >= 3.8.19
54
+
55
+
56
+ === pre-installation
57
+ pre-installation script are run before update of the module, and allow to fix data before module update or create column to avoid compute.
58
+
59
+ each pre_installation script for a given ticket must be in a python file called `pre_<ticket_number>.py`, located in a 'migrations/auto' directory in your module. +
60
+ Scripts are played in alphabetical order of the file's names. Inside a file, method are played in declaration order, unless priority parameter is set in the decorator.
61
+ Lowest number is highest priority (1 more priority than 25). +
62
+
63
+ you must import :
64
+ `from odoo.addons.auto_migration.mangono_migration import auto_migrate`
65
+
66
+ the functions must be decorated by `@auto_migrate` and have this signature :
67
+ `def xxxxx(self)` or for backward compatibility `def xxxxx(self, has_run):`
68
+
69
+ self is a factice object (not usual odoo self), but self.env is a real odoo env, `self.env['model']` is able to access odoo models, `self.env.cr` or `self.cr` can be used to perform request +
70
+ self.has_run is a boolean true if the script has already been run (same for has_run parameter if used)
71
+
72
+ EXAMPLE:
73
+ [source,python]
74
+ ----
75
+ @migrate_mangono(run_always=False, priority=10, allowed_to_fail=True)
76
+ def pre_t1245(self):
77
+ # the function will always get self.has_run = False,
78
+ # because it will be called only if it has not run yet
79
+ self.env['my.model'].search(my_domain).unlink # with allowed_to_fail, if this fails, the script
80
+ # action will be rollbacked but update goes on
81
+ ----
82
+
83
+ SIMPLE EXAMPLE:
84
+ [source,python]
85
+ ----
86
+ @migrate_mangono
87
+ def pre_t5432(self):
88
+ self.cr.execute("UPDATE xxxx")
89
+ ----
90
+
91
+ if the optional decorator parameter `run_always` is set to True
92
+ the method must use the has_run bool to check if the script has already been run on this base or check if
93
+ there is something to do before doing it. (_default value of run_always is False_)
94
+
95
+ in case of exception the script is rollbacked, it is not written as run in the `auto_migration` table and installation will stop, other scripts will not be executed
96
+
97
+ if the optional decorator parameter `allowed_to_fail` is set to `True`, the script failure will not interrupt installation or update process. The DB is rollbacked to the state at the beginning of the script execution. Failure logs will be reported in table logs and a warning level log is issued in odoo logs. (_default is False_)
98
+
99
+ The optional decorator parameter `priority` can be set to a priority from 1 to 9999 (_default_) to decide the order in which script from the same python file will be played (priority 1 is played first).
100
+
101
+ NOTE: old decorator @migrate_mangono is kept for backaward compatibility
102
+
103
+ === post-installation
104
+
105
+ post-installation script are run after the module is installed/updated, before module translation are loaded, and before `post_init_hook`
106
+
107
+ python file must be called `post_<Ticket_number>`
108
+
109
+ decorator use is the same
110
+
111
+ IMPORTANT: when a post script fails, all post action are rollbacked, but pre action are committed
112
+
113
+ === end
114
+
115
+ same as post, with end_XXXX +
116
+ Ces scripts sont joués après l'installation/mise à jour de tous les modules.
117
+
118
+ === afterbase
119
+ Use to play a script just after base is updated, before any other modules are updated.
120
+
121
+ To allow this, your script must reside in any installed modules (usually project_erp) and the file must be named `afterbase_xxxx.py`. You must run `update base`
122
+
123
+ You cannot use models in this script, everything must be done with SQL
124
+
125
+ === jobification :
126
+
127
+ you can start job from the script by calling `with_delay` as usual. +
128
+ Note that job will not start during update phase. They will only start when the instance is restarted after the update.
129
+
130
+ === helper function
131
+ `table_exists_in_db(cr, tablename)` can be used to check if a table exists in database
132
+
133
+ `column_exists_in_db(cr, table_name, column_name)` can be used to check if a column exists in the given table.
134
+
135
+
136
+ == Script example
137
+
138
+ [source,python]
139
+ ----
140
+
141
+ import logging
142
+ from odoo.addons.auto_migration.mangono_migration import auto_migrate
143
+
144
+ _logger = logging.getLogger(__name__)
145
+
146
+
147
+ @auto_migrate(run_always=False, allowed_to_fail=False)
148
+ def update_rh_rights(self, has_run):
149
+ # Le script consiste à :
150
+ # - extraire tous les utilisateurs faisant partie du groupe Admin/Droits d'accès
151
+ # - les faire appartenir au groupe RH/Formation.
152
+ group_admin = self.env.ref('base.group_erp_manager')
153
+ group_training = self.env.ref(
154
+ 'xxx_donnees_ref.group_xxx_training_manager')
155
+ users = group_admin.users
156
+ for user in users:
157
+ _logger.info(u"user %s added to RRH/formation group", user)
158
+ user.groups_id = [(4, group_training.id)]
159
+ ----
@@ -0,0 +1,7 @@
1
+ def create_migration_channel(cr):
2
+ """create if necessary the root.migration_job channel for execution of job launched by migration"""
3
+ cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname='queue_job_channel'")
4
+ if cr.fetchall(): # queue_job_channel may not exists if module connector is not installed
5
+ cr.execute("""SELECT COUNT(*) FROM queue_job_channel WHERE NAME = 'root.migration_jobs'""")
6
+ if not cr.fetchall()[0][0]: # the channel doesn(t exist yet, let's create it
7
+ cr.execute("""INSERT INTO queue_job_channel (name, parent_id) VALUES ('root.migration_jobs', 1)""")
@@ -0,0 +1,16 @@
1
+ # Translation of Odoo Server.
2
+ # This file contains the translation of the following modules:
3
+ #
4
+ msgid ""
5
+ msgstr ""
6
+ "Project-Id-Version: Odoo Server 8.0\n"
7
+ "Report-Msgid-Bugs-To: \n"
8
+ "POT-Creation-Date: 2019-03-12 11:13+0000\n"
9
+ "PO-Revision-Date: 2019-03-12 11:13+0000\n"
10
+ "Last-Translator: <>\n"
11
+ "Language-Team: \n"
12
+ "MIME-Version: 1.0\n"
13
+ "Content-Type: text/plain; charset=UTF-8\n"
14
+ "Content-Transfer-Encoding: \n"
15
+ "Plural-Forms: \n"
16
+
@@ -0,0 +1,415 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import inspect
5
+ import logging
6
+ import os
7
+ from inspect import getmembers, isfunction
8
+ from operator import attrgetter
9
+ from typing import Any
10
+
11
+ from environ_odoo_config.odoo_utils import get_server_wide_modules
12
+ from typing_extensions import Self
13
+
14
+ import odoo
15
+
16
+ Path = str
17
+ MAJOR = odoo.release.version_info[0]
18
+ MAJOR_STR = odoo.release.serie
19
+ MIGRATION_DIR = [
20
+ "auto_migrations",
21
+ "ndp_migration",
22
+ "mangono_migration", # Name of the directory where the script must be located
23
+ ]
24
+ if MAJOR < 19:
25
+ from odoo import (
26
+ SUPERUSER_ID,
27
+ api,
28
+ fields,
29
+ registry, # pylint: disable=ungrouped-imports; pylint: disable=ungrouped-imports
30
+ )
31
+ else:
32
+ from odoo import (
33
+ SUPERUSER_ID,
34
+ api,
35
+ fields,
36
+ )
37
+
38
+ # from odoo.addons.queue_job.models import with_delay
39
+ from odoo.modules.migration import MigrationManager # pylint: disable=ungrouped-imports
40
+ from odoo.modules.module import (
41
+ get_module_path,
42
+ get_modules, # pylint: disable=ungrouped-imports; pylint: disable=ungrouped-imports
43
+ )
44
+ from odoo.tools import config # pylint: disable=ungrouped-imports
45
+
46
+ _logger = logging.getLogger(__name__)
47
+
48
+
49
+ def _is_valid_migration_file_for_stage(stage: str, name: str) -> bool:
50
+ """
51
+ :param name: filename without path
52
+ :return: True is filename is in the form : stage_XXXX.py (case insensitive)
53
+ """
54
+ _, ext = os.path.splitext(name)
55
+ return ext.lower() == ".py" and name.lower().startswith((stage + "_").lower())
56
+
57
+
58
+ def table_exists_in_db(cr, tablename: str) -> bool:
59
+ """
60
+ :param cr: a cursor object
61
+ :param tablename: name of the table (stock_move)
62
+ :return: True if the table exists in database, False otherwise
63
+ """
64
+ cr.execute(
65
+ """
66
+ SELECT EXISTS (
67
+ SELECT FROM pg_catalog.pg_class c
68
+ JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
69
+ WHERE n.nspname = 'public'
70
+ AND c.relname = %s)
71
+ """,
72
+ (tablename,),
73
+ )
74
+ return cr.fetchone()[0]
75
+
76
+
77
+ def column_exists_in_db(cr, table_name: str, column_name: str) -> bool:
78
+ """
79
+ :param cr: cursor object
80
+ :param table_name: 'stock_move' for exmaple
81
+ :param column_name: 'location_id' for example
82
+ :return: True if column exists in table, false if columns or table doesn't exist
83
+ """
84
+ if table_exists_in_db(cr, table_name):
85
+ cr.execute(
86
+ """
87
+ SELECT EXISTS(SELECT *
88
+ FROM INFORMATION_SCHEMA.COLUMNS
89
+ WHERE TABLE_NAME = %s
90
+ AND COLUMN_NAME = %s);
91
+ """,
92
+ (table_name, column_name),
93
+ )
94
+ return cr.fetchone()[0]
95
+ return False
96
+
97
+
98
+ def mangono_migrate_module(self, pkg, stage: str):
99
+ # this function is called from loading.py, self gives access to cr and graph
100
+ _logger.debug("auto_migrate_module stage %s module %s", pkg.name, stage)
101
+ if pkg.name not in ["base", "auto_migration"] and not config["test_enable"]:
102
+ run_migration_script(self, stage, pkg)
103
+ result = MigrationManager.origin_migrate_module(self, pkg, stage)
104
+ return result
105
+
106
+
107
+ # ruff: noqa: PERF203
108
+ def get_module_filetree(module_path: Path) -> dict[str, Path]:
109
+ """return a dict of file: absolute_path for every file in migration directory of module_path"""
110
+ res = {}
111
+ for directory in MIGRATION_DIR:
112
+ try:
113
+ res.update(
114
+ {
115
+ f: os.path.join(module_path, directory, f)
116
+ for f in sorted(os.listdir(os.path.join(module_path, directory)))
117
+ }
118
+ )
119
+ except FileNotFoundError:
120
+ continue
121
+ return res
122
+
123
+
124
+ def just_after_base(self, stage: str, pkg) -> None:
125
+ """
126
+ :param stage: the migration stage
127
+ :param pkg: a Package object
128
+ :return: if the package is the first one after base, looks for scripts with stage 'afterbase' in all migration dirs
129
+ During an "update base", this will execute these migration script beginning with afterbase_ ...
130
+ before any other community, common, ... modules are loaded (only base is loaded)
131
+ """
132
+ if MAJOR < 19:
133
+ list_pkg = list(pkg.graph)
134
+ else:
135
+ list_pkg = list(pkg.module_graph)
136
+ if stage == "pre" and list_pkg[0].name == "base" and list_pkg[1] == pkg: # we are the first package after base
137
+ mods = get_modules()
138
+ for mod in mods:
139
+ self.cr.execute("""select state from ir_module_module where name = %s""", (mod,))
140
+ state = self.cr.fetchone()
141
+ if state and state[0] in [
142
+ "installed",
143
+ "to upgrade",
144
+ ]: # we check only installed modules
145
+ module_path = get_module_path(mod)
146
+ file_tree = get_module_filetree(module_path)
147
+ if file_tree:
148
+ run_scripts_of_module(self, file_tree, "afterbase")
149
+
150
+
151
+ def run_migration_script(self, stage: str, pkg) -> None:
152
+ _logger.info("Running migration script %s stage: %s", pkg.name, stage)
153
+ if MAJOR >= 18:
154
+ _registry = odoo.modules.registry.Registry(self.cr.dbname)
155
+ else:
156
+ _registry = registry(self.cr.dbname)
157
+ just_after_base(self, stage, pkg) # really working ??
158
+ module_path = get_module_path(pkg.name)
159
+ file_tree = get_module_filetree(module_path)
160
+ if file_tree:
161
+ if stage == "pre":
162
+ if MAJOR >= 19:
163
+ _registry._setup_models__(self.cr)
164
+ else:
165
+ _registry.setup_models(self.cr)
166
+ _logger.debug("=== found auto migration directory in %s for stage %s", pkg.name, stage)
167
+ run_scripts_of_module(self, file_tree, stage)
168
+
169
+
170
+ def run_scripts_of_module(self, file_tree: dict[str, Path], stage: str) -> None:
171
+ """run all script in this module matching stage (python file beginning with stage_)"""
172
+ _logger.debug("=== running auto migration module %s", stage)
173
+ for filename, file_path in file_tree.items():
174
+ # python file must be directly under auto_migrations directory
175
+ if _is_valid_migration_file_for_stage(stage, filename):
176
+ file_without_ext = os.path.splitext(filename)[0]
177
+ # load and execute _pre_migrate()
178
+ py_mod = load_module_from_file(file_without_ext, file_path)
179
+ # we recognize the python function to run because the @auto_migrate wrapper adds an
180
+ # 'is_auto_migration' attribute to it, <migrate_mangono> is imported by the import, must not be run
181
+ scripts = [
182
+ f[1]
183
+ for f in getmembers(py_mod, isfunction)
184
+ if f[0] != "migrate_mangono"
185
+ and (hasattr(f[1], "is_mangono_migration") or hasattr(f[1], "is_auto_migration"))
186
+ ]
187
+ for method in sorted(scripts, key=attrgetter("priority")):
188
+ module = file_path.split("/")[-3]
189
+ ident = "{module}.{method}".format(module=module or "", method=method.__name__)
190
+ script = MangonoMigration.get_or_create(self.cr, ident)
191
+ script.run_method_if_needed(method)
192
+
193
+
194
+ def patch_migration() -> None:
195
+ if "auto_migration" in get_server_wide_modules(MAJOR_STR) and not config["test_enable"]:
196
+ MigrationManager.origin_migrate_module = (
197
+ hasattr(MigrationManager, "origin_migrate_module")
198
+ and MigrationManager.origin_migrate_module
199
+ or MigrationManager.migrate_module
200
+ )
201
+ MigrationManager.migrate_module = mangono_migrate_module
202
+ if "openupgrade_framework" in get_server_wide_modules(MAJOR_STR) and hasattr(
203
+ MigrationManager.origin_migrate_module, "_original_method"
204
+ ):
205
+ # When openupgrade is activated with mangono_migration we need to copy _original_method ref
206
+ mangono_migrate_module._original_method = MigrationManager.origin_migrate_module._original_method # pyright: ignore[reportFunctionMemberAccess]
207
+ MigrationManager._logger = hasattr(MigrationManager, "_logger") and MigrationManager._logger or _logger
208
+ _logger.info("module auto_migration enabled")
209
+
210
+
211
+ class MangonoMigration: # pylint: disable=useless-object-inheritance
212
+ """
213
+ Provide service for pre-installation and post-installation scripts
214
+ see <documentation.adoc> for detailed documentation
215
+ """
216
+
217
+ _description = "Automation scripting system for pre and post installation scripting"
218
+
219
+ def __init__(
220
+ self,
221
+ ticket: str,
222
+ cr,
223
+ md5: str | None = None,
224
+ has_run: bool = False,
225
+ create_date=None,
226
+ write_date=None,
227
+ log: str | None = None,
228
+ ):
229
+ """prepare the table for script execution recording if necessary"""
230
+ super().__init__()
231
+ self.ticket = ticket
232
+ self.md5 = md5 or ""
233
+ self.log = log or ""
234
+ self.has_run = has_run
235
+ self.create_date = create_date or fields.Datetime.now()
236
+ self.write_date = write_date or fields.Datetime.now()
237
+ self.cr = cr
238
+
239
+ @classmethod
240
+ def create_table_if_needed(cls, cr) -> None:
241
+ """Keep as classmethod because used in get_or_create to create an instance of MangonoMigration"""
242
+ cr.execute("ALTER TABLE IF EXISTS ndp_migration RENAME TO auto_migration")
243
+ cr.execute("ALTER TABLE IF EXISTS mangono_migration RENAME TO auto_migration")
244
+ cr.execute(
245
+ """CREATE TABLE IF NOT EXISTS auto_migration ( id serial NOT NULL
246
+ CONSTRAINT
247
+ mangono_migration_pkey
248
+ PRIMARY
249
+ KEY,
250
+ create_date
251
+ timestamp,
252
+ write_date
253
+ timestamp,
254
+ ticket
255
+ varchar
256
+ NOT
257
+ NULL
258
+ CONSTRAINT
259
+ mangono_migration_unique_ticket
260
+ UNIQUE,
261
+ md5
262
+ varchar,
263
+ has_run bool,
264
+ log varchar
265
+ );"""
266
+ )
267
+
268
+ def set_run(self) -> None:
269
+ """record the fact that this script has been run"""
270
+ md5 = ""
271
+ self.create_table_if_needed(self.cr)
272
+ date_str = fields.Datetime.to_string(fields.Datetime.now())
273
+ _logger.debug("script %s has run succesfully", self.ticket)
274
+ self.cr.execute(
275
+ """UPDATE auto_migration SET has_run = TRUE, md5 = %s, write_date = %s, log = %s
276
+ WHERE ticket = %s""",
277
+ (md5, date_str, self.log + "\n" + date_str + " has_run", self.ticket),
278
+ )
279
+
280
+ @classmethod
281
+ def get_or_create(cls, cr, ident: str) -> Self:
282
+ """return MangonoMigration object from existing record of database, create it in base if it doesn't exist"""
283
+
284
+ def get_value() -> dict[str, Any]:
285
+ # odoo himself use string formating to insert table name into query
286
+ cls.create_table_if_needed(cr)
287
+ cr.execute("""SELECT * FROM auto_migration WHERE ticket = %s""", (ident,))
288
+ return cr.dictfetchone()
289
+
290
+ res = get_value()
291
+ if not res:
292
+ cr.execute(
293
+ """INSERT INTO auto_migration
294
+ (ticket, has_run, create_date, write_date)
295
+ VALUES (%s, FALSE, %s, %s)""",
296
+ (ident, fields.Datetime.now(), fields.Datetime.now()),
297
+ )
298
+ res = get_value()
299
+ res.pop("id")
300
+ return MangonoMigration(cr=cr, **res)
301
+
302
+ def log_error(self, msg: str) -> None:
303
+ self.create_table_if_needed(self.cr)
304
+ self.cr.execute(
305
+ """UPDATE auto_migration SET log = %s WHERE ticket = %s""",
306
+ (
307
+ f"{self.log}\n{fields.Datetime.to_string(fields.Datetime.now())} : {msg}",
308
+ self.ticket,
309
+ ),
310
+ )
311
+
312
+ def run_method_if_needed(self, method: callable) -> None:
313
+ _logger.info("preparing to run %s", method.__name__)
314
+ try:
315
+ if method.run_always or not self.has_run:
316
+ if method.allowed_to_fail:
317
+ with self.cr.savepoint(): # when failed, changes are rollbacked
318
+ _logger.info(
319
+ "auto_migration executing %s allowed to fail",
320
+ method.__name__,
321
+ )
322
+ self._call_method(method)
323
+ else:
324
+ _logger.info(
325
+ "auto_migration executing %s not allowed to fail",
326
+ method.__name__,
327
+ )
328
+ self._call_method(method)
329
+ # pylint: disable=broad-except
330
+ except Exception as err:
331
+ _logger.warning("Error during migration <%s>: %s", self.ticket, err)
332
+ self.log_error(f"{err}")
333
+ if not method.allowed_to_fail:
334
+ # note that the self allowed_to_fail which fails will have an error log, but not set to run
335
+ raise
336
+ else:
337
+ if method.run_always or not self.has_run:
338
+ self.set_run()
339
+
340
+ def _call_method(self, method: callable) -> None:
341
+ parameters = inspect.signature(method).parameters
342
+ if len(parameters) > 2:
343
+ self.log_error(f"{method.__name__} wrong signature (too many arguments)")
344
+ elif len(parameters) == 2 and parameters.get("has_run"):
345
+ # old signature with parameter has_run
346
+ method(ProxySelf(self.cr, self.has_run), self.has_run)
347
+ else: # cas où la méthode ne prend pas has_run en paramètre
348
+ method(ProxySelf(self.cr, self.has_run))
349
+
350
+
351
+ class ProxySelf: # pylint: disable=useless-object-inheritance
352
+ """provide a factice object with env attribute to be able to use self.env, self.env.cr in script
353
+ self.has_run is true when the script has already been run (for run_always script)
354
+ """
355
+
356
+ def __init__(self, cr, has_run: bool = False):
357
+ super().__init__()
358
+ env = api.Environment(cr, SUPERUSER_ID, {})
359
+ self.env = env
360
+ self.cr = cr
361
+ self.uid = SUPERUSER_ID
362
+ self.context = self.env.context
363
+ self.has_run = has_run
364
+
365
+
366
+ def auto_migrate(run_always: bool | callable = False, priority: int = 9999, allowed_to_fail: bool = False) -> callable:
367
+ def _migrate_mangono(method: callable) -> callable:
368
+ """This decorator decorate the method with the argument declared in the call to auto_migrate()
369
+ and return the decorated method without changing the signature"""
370
+ method.run_always = run_always
371
+ method.priority = priority
372
+ method.allowed_to_fail = allowed_to_fail
373
+ method.is_auto_migration = True
374
+ return method
375
+
376
+ if callable(run_always):
377
+ # use case of @auto_migrate without bracket, in this case the first argument is the method, we need
378
+ # to implement default value for missings arguments
379
+ method = run_always
380
+ run_always = False
381
+ priority = 9999
382
+ allowed_to_fail = False
383
+ return _migrate_mangono(method)
384
+
385
+ return _migrate_mangono
386
+
387
+
388
+ migrate_mangono = auto_migrate
389
+
390
+
391
+ ##############################################################################
392
+ # Copyright (c) 2013-2018, Lawrence Livermore National Security, LLC.
393
+ # Produced at the Lawrence Livermore National Laboratory.
394
+ # Extract from spack
395
+ # This program is free software; you can redistribute it and/or modify
396
+ # it under the terms of the GNU Lesser General Public License (as
397
+ # published by the Free Software Foundation) version 2.1, February 1999.
398
+ ##############################################################################
399
+ def load_module_from_file(module_name, module_path):
400
+ """Loads a python module from the path of the corresponding file.
401
+ Args:
402
+ module_name (str): namespace where the python module will be loaded,
403
+ e.g. ``foo.bar``
404
+ module_path (str): path of the python file containing the module
405
+ Returns:
406
+ A valid module object
407
+ Raises:
408
+ ImportError: when the module can't be loaded
409
+ FileNotFoundError: when module_path doesn't exist
410
+ """
411
+ spec = importlib.util.spec_from_file_location(module_name, module_path)
412
+ module = importlib.util.module_from_spec(spec)
413
+ spec.loader.exec_module(module)
414
+
415
+ return module