feat(jupyter): refactor Jupyter integration to use kernel startup scripts and add papermill support

This commit is contained in:
Janik von Rotz committed 2026-05-19 09:18:26 +02:00
1 parent bca7ddba1d
commit c59ca7726f
8 files changed
+220 -253

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""
Initialize a Jupyter Lab directory with Odoo bootstrap files.
Creates:
- <path>/.ipython/profile_default/startup/00-odoo.py
- <path>/main.ipynb
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
STARTUP_TEMPLATE = r'''
import os
import atexit
import odoo
from odoo import api
from odoo.modules.registry import Registry
from odoo.tools import config
db_name = os.environ["DB_NAME"]
config["db_name"] = db_name
config["db_host"] = os.environ.get("PGHOST", "localhost")
config["db_port"] = os.environ.get("PGPORT", "5432")
config["db_user"] = os.environ.get("PGUSER", "odoo")
config["db_password"] = os.environ.get("PGPASSWORD", "odoo")
registry = Registry(db_name)
cr = registry.cursor()
uid = 2
ctx = api.Environment(cr, uid, {})["res.users"].context_get()
env = api.Environment(cr, uid, ctx)
self = env.user
ip = get_ipython()
ip.push({
"odoo": odoo,
"env": env,
"cr": cr,
"uid": uid,
"self": self,
"api": api,
})
@atexit.register
def _cleanup():
try:
cr.rollback()
cr.close()
except Exception:
pass
'''
def create_notebook(path: Path) -> None:
if path.exists():
print(f"Notebook already exists, skipping: {path}")
return
notebook = {
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Odoo Notebook\n",
"\n",
"Available automatically:\n",
"\n",
"- `env`\n",
"- `cr`\n",
"- `odoo`\n",
"- `self`\n",
"- `uid`\n",
"- `api`\n",
],
},
{
"cell_type": "code",
"execution_count": None,
"metadata": {},
"outputs": [],
"source": ["env.user.name"],
},
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3",
},
"language_info": {
"name": "python",
},
},
"nbformat": 4,
"nbformat_minor": 5,
}
path.write_text(json.dumps(notebook, indent=2))
print(f"Created notebook: {path}")
def create_startup_script(ipython_dir: Path) -> None:
startup_dir = ipython_dir / "profile_default" / "startup"
startup_dir.mkdir(parents=True, exist_ok=True)
startup_file = startup_dir / "00-odoo.py"
startup_file.write_text(STARTUP_TEMPLATE)
print(f"Created startup script: {startup_file}")
def main() -> None:
parser = argparse.ArgumentParser(
description="Initialize a directory for Odoo Jupyter Lab."
)
parser.add_argument("path", help="Directory to initialize (e.g. tmp)")
args = parser.parse_args()
notebook_dir = Path(args.path).resolve()
notebook_dir.mkdir(parents=True, exist_ok=True)
ipython_dir = notebook_dir / ".ipython"
print()
print("Initializing Odoo Jupyter directory")
print(f"Path: {notebook_dir}")
print()
create_startup_script(ipython_dir)
create_notebook(notebook_dir / "main.ipynb")
print()
print("Done. Run 'task start-odoo-jupyter <path> [database]' to launch.")
if __name__ == "__main__":
main()
+7 -76
View File
@@ -10,75 +10,6 @@ import sys
from pathlib import Path
STARTUP_TEMPLATE = r'''
import os
import atexit
import odoo
from odoo import api
from odoo.modules.registry import Registry
from odoo.tools import config
db_name = os.environ["DB_NAME"]
config["db_name"] = db_name
config["db_host"] = os.environ.get("PGHOST", "localhost")
config["db_port"] = os.environ.get("PGPORT", "5432")
config["db_user"] = os.environ.get("PGUSER", "odoo")
config["db_password"] = os.environ.get("PGPASSWORD", "odoo")
registry = Registry(db_name)
cr = registry.cursor()
uid = 2
ctx = api.Environment(cr, uid, {})["res.users"].context_get()
env = api.Environment(cr, uid, ctx)
self = env.user
ip = get_ipython()
ip.push({
"odoo": odoo,
"env": env,
"cr": cr,
"uid": uid,
"self": self,
"api": api,
})
@atexit.register
def _cleanup():
try:
cr.rollback()
cr.close()
except Exception:
pass
print("=" * 60)
print(f"Odoo ready: {db_name}")
print(f"User: {env.user.name}")
print(f"Models: {len(env.registry.models)}")
print("=" * 60)
'''
def create_startup_script(ipython_dir: Path) -> None:
startup_dir = ipython_dir / "profile_default" / "startup"
startup_dir.mkdir(parents=True, exist_ok=True)
startup_file = startup_dir / "00-odoo.py"
startup_file.write_text(STARTUP_TEMPLATE)
def main() -> None:
parser = argparse.ArgumentParser(
description="Execute Jupyter notebook with papermill"
@@ -113,17 +44,21 @@ def main() -> None:
ipython_dir = project_root / ".ipython"
create_startup_script(ipython_dir)
startup_script = ipython_dir / "profile_default" / "startup" / "00-odoo.py"
if not startup_script.exists():
print(
f"ERROR: Directory not initialized. Run 'task init-odoo-jupyter {project_root}' first.",
file=sys.stderr,
)
sys.exit(1)
env = os.environ.copy()
env["DB_NAME"] = db_name
env.setdefault("PGHOST", "localhost")
env.setdefault("PGPORT", "5432")
env.setdefault("PGUSER", "odoo")
env.setdefault("PGPASSWORD", "odoo")
env["IPYTHONDIR"] = str(ipython_dir)
output_path = notebook_path
@@ -136,9 +71,7 @@ def main() -> None:
]
print()
print("=" * 60)
print("Executing notebook")
print("=" * 60)
print(f"Notebook : {notebook_path}")
print(f"Database : {db_name}")
print()
@@ -162,9 +95,7 @@ def main() -> None:
sys.exit(1)
print()
print("=" * 60)
print("Notebook executed successfully")
print("=" * 60)
print(output_path)
print()
+23 -151
View File
@@ -2,192 +2,64 @@
"""
Start Jupyter Lab with a preloaded Odoo environment.
The notebook itself contains NO bootstrap code.
Everything is initialized in the kernel startup process.
Expects the directory to already be initialized via init-odoo-jupyter.
"""
from __future__ import annotations
import argparse
import json
import os
import secrets
import subprocess
from pathlib import Path
STARTUP_TEMPLATE = r'''
import os
import atexit
import odoo
from odoo import api
from odoo.modules.registry import Registry
from odoo.tools import config
db_name = os.environ["DB_NAME"]
config["db_name"] = db_name
config["db_host"] = os.environ.get("PGHOST", "localhost")
config["db_port"] = os.environ.get("PGPORT", "5432")
config["db_user"] = os.environ.get("PGUSER", "odoo")
config["db_password"] = os.environ.get("PGPASSWORD", "odoo")
registry = Registry(db_name)
cr = registry.cursor()
uid = 2
ctx = api.Environment(cr, uid, {})["res.users"].context_get()
env = api.Environment(cr, uid, ctx)
self = env.user
ip = get_ipython()
ip.push({
"odoo": odoo,
"env": env,
"cr": cr,
"uid": uid,
"self": self,
"api": api,
})
@atexit.register
def _cleanup():
try:
cr.rollback()
cr.close()
except Exception:
pass
print("=" * 60)
print(f"Odoo ready: {db_name}")
print(f"User: {env.user.name}")
print(f"Models: {len(env.registry.models)}")
print("=" * 60)
'''
def create_notebook(path: Path) -> None:
"""
Create a clean notebook with zero bootstrap code.
"""
if path.exists():
return
notebook = {
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Odoo Notebook\n",
"\n",
"Available automatically:\n",
"\n",
"- `env`\n",
"- `cr`\n",
"- `odoo`\n",
"- `self`\n",
"- `uid`\n",
"- `api`\n",
],
},
{
"cell_type": "code",
"execution_count": None,
"metadata": {},
"outputs": [],
"source": [
"env.user.name"
],
},
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3",
},
"language_info": {
"name": "python",
},
},
"nbformat": 4,
"nbformat_minor": 5,
}
path.write_text(json.dumps(notebook, indent=2))
def create_startup_script(ipython_dir: Path) -> None:
"""
Create IPython startup hook.
"""
startup_dir = ipython_dir / "profile_default" / "startup"
startup_dir.mkdir(parents=True, exist_ok=True)
startup_file = startup_dir / "00-odoo.py"
startup_file.write_text(STARTUP_TEMPLATE)
def main() -> None:
parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser(
description="Start Jupyter Lab with Odoo kernel bootstrap."
)
parser.add_argument("path", help="Directory previously initialized by init-odoo-jupyter")
parser.add_argument(
"database",
nargs="?",
default=os.environ.get("DB_NAME", "odoo"),
)
parser.add_argument(
"token",
nargs="?",
default=None,
)
args = parser.parse_args()
db_name = args.database
script_dir = Path(__file__).resolve().parent
project_root = script_dir.parent
notebook_dir = project_root / "tmp"
notebook_dir.mkdir(exist_ok=True)
notebook_dir = Path(args.path).resolve()
ipython_dir = notebook_dir / ".ipython"
env = os.environ.copy()
if not (notebook_dir / "main.ipynb").exists():
print(f"Error: {notebook_dir} does not look initialized.")
print("Run 'task init-odoo-jupyter <path>' first.")
raise SystemExit(1)
env["DB_NAME"] = db_name
token = args.token or secrets.token_hex(16)
env = os.environ.copy()
env["DB_NAME"] = args.database
env.setdefault("PGHOST", "localhost")
env.setdefault("PGPORT", "5432")
env.setdefault("PGUSER", "odoo")
env.setdefault("PGPASSWORD", "odoo")
env["IPYTHONDIR"] = str(ipython_dir)
create_startup_script(ipython_dir)
create_notebook(notebook_dir / "main.ipynb")
print()
print("=" * 60)
print("Starting Jupyter Lab with Odoo")
print("=" * 60)
print(f"Database : {db_name}")
print(f"Database : {args.database}")
print(f"Notebook : {notebook_dir}")
print()
cmd = [
"jupyter-lab",
f"--ServerApp.root_dir={notebook_dir}",
"--ServerApp.open_browser=True",
f"--ServerApp.token={token}",
"--ServerApp.open_browser=False",
"--ServerApp.port=8888",
]
-1
View File
@@ -45,5 +45,4 @@ rl-renderPM
# Jupyter Notebook
jupyter
ipykernel
papermill
@@ -0,0 +1,5 @@
<data priority="50">
<list position="attributes">
<attribute name="default_order">create_date asc, priority desc, sla_deadline, id</attribute>
</list>
</data>
+26 -10
View File
@@ -156,7 +156,6 @@ help-table() {
rows+=("psql|[db]|Start interactive psql shell.")
rows+=("restart|[name]|Restart container.")
rows+=("run|[name][cmd]|Run container with command.")
rows+=("run-jupyter-notebook|[path][db]|Run juypter notebook with papermill.")
rows+=("shell|[db][code]|Start interactive odoo shell or run code.")
rows+=("source||Source the Python virtual env.")
rows+=("start|[name][db]|Start service. Use list to show services.")
@@ -184,7 +183,11 @@ help-table() {
rows+=("prepare-migration-brach|[path][module][version]|Create migration branch for a module.")
rows+=("migrate-module|[path]|Migrate module code from to target Odoo version.")
rows+=("upgrade-odoo|[env][step]|Run the Odoo upgrade for a target environment.")
rows+=("===JUPYTER===| | ")
rows+=("init-odoo-jupyter|[path]|Init jypyter lab directory.")
rows+=("start-odoo-jupyter|[path][db]|Start juypter lab in directory.")
rows+=("run-jupyter-notebook|[path][db]|Run juypter notebook with papermill.")
for row in "${rows[@]}"; do
IFS='|' read -r cmd opt desc <<< "$row"
(( ${#cmd} > max_cmd )) && max_cmd=${#cmd}
@@ -724,9 +727,6 @@ build-odoo() {
local python_version="3.12"
;;
"19.0")
local python_version="3.13"
;;
*)
local python_version="3.12"
;;
esac
@@ -1285,11 +1285,6 @@ start() {
echo 'Open http://localhost:8080 url in your browser.'
open-url-with-delay "http://localhost:8080" 2 & $COMPOSE_COMMAND up -d odoo
fi
if [[ "$1" =~ "jupyter" ]]; then
# jupyter lab
./bin/start-odoo-jupyter "$database"
fi
}
debug() {
@@ -1334,6 +1329,27 @@ run() {
$COMPOSE_COMMAND run --rm "$1" "$2" "$3"
}
init-odoo-jupyter() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
./bin/init-odoo-jupyter "$1"
}
start-odoo-jupyter() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
exit
fi
local database="$2"
if [[ -z "$database" ]]; then
database="$DB_NAME"
fi
local token="odoo"
./bin/start-odoo-jupyter "$1" "$database" "$token"
}
run-jupyter-notebook() {
if [[ -z "$1" ]]; then
echo '$1/path is empty.'
+14 -14
View File
@@ -3,28 +3,28 @@
url = git@github.com:odoo/odoo.git
branch = 19.0
shallow = true
commit = 82124886fdb3485ef9dc089e5c33b7c803030d5e
commit = 32301e8acba31140408bead9677c466baf0e0a45
[submodule "enterprise"]
path = enterprise
url = git@github.com:odoo/enterprise.git
branch = 19.0
shallow = true
commit = 64f813dab727d76286c1ff6c80c08cb6a6737b49
commit = af3e13b2881c7647aed022f29a187149c95bb255
[submodule "themes"]
path = themes
url = git@github.com:odoo/design-themes.git
branch = 19.0
commit = 7599a4bd2862e3fcd672de78397d05bcb135df70
commit = 6e142b7abc66777bb9f6f5d124609b97ac93e7a5
[submodule "addons/server_tools"]
path = addons/server_tools
url = git@github.com:Mint-System/Odoo-Apps-Server-Tools.git
branch = 19.0
commit = 41841bf3c5508390676ababdf0cc7df8951ba589
commit = 53e65405dd605804b75ca3a3dfc40ef4970718c1
[submodule "oca/partner-contact"]
path = oca/partner-contact
url = git@github.com:OCA/partner-contact.git
branch = 19.0
commit = b6b6652548fb79c95c4f08d725a0e83356b34c99
commit = b54008efb460beec76eb42347238f1618f6df32a
[submodule "addons/management_system"]
path = addons/management_system
url = git@github.com:Mint-System/Odoo-Apps-Management-System.git
@@ -34,7 +34,7 @@
path = addons/sale_workflow
url = git@github.com:Mint-System/Odoo-Apps-Sale-Workflow.git
branch = 19.0
commit = e4796bc9cd84628aac3616ba3e146a4d418a64be
commit = 2baad67347f539fa9833be1f1ec2be94ccf603b0
[submodule "addons/account_invoicing"]
path = addons/account_invoicing
url = git@github.com:Mint-System/Odoo-Apps-Account-Invoicing.git
@@ -44,22 +44,22 @@
path = addons/purchase_workflow
url = git@github.com:Mint-System/Odoo-Apps-Purchase-Workflow.git
branch = 19.0
commit = 7bd3d7a02f3fadf71ae58ad34f2523188d2cf3f8
commit = 8b4f075f6b0c606e05bf5bd3e9fbb7e560d50ba5
[submodule "addons/stock_logistics_workflow"]
path = addons/stock_logistics_workflow
url = git@github.com:Mint-System/Odoo-Apps-Stock-Logistics-Workflow.git
branch = 19.0
commit = b250302f012da0ca116855e7ba641c6eb75578ad
commit = 6587b4a8e411ff350de822dc2a5e17484121214d
[submodule "oca/sale-workflow"]
path = oca/sale-workflow
url = git@github.com:OCA/sale-workflow
branch = 19.0
commit = 87113dc142dab74f34d242904c739a14cfa05b3c
commit = d2121f9419e7a1b87382ca06b278b4b0cf4befde
[submodule "oca/server-tools"]
path = oca/server-tools
url = git@github.com:OCA/server-tools.git
branch = 19.0
commit = 793dd13b1fc60a43b091b7e156aea95c8bc2011d
commit = 3f56112d5d507ba6e6cff73a52309e4368008f66
[submodule "addons/spreadsheet"]
path = addons/spreadsheet
url = git@github.com:Mint-System/Odoo-Apps-Spreadsheet.git
@@ -69,17 +69,17 @@
path = addons/product_attribute
url = git@github.com:Mint-System/Odoo-Apps-Product-Attribute.git
branch = 19.0
commit = 4cd5253da71c66d757ed292aa7fd18e64eef8375
commit = 225d3bdffe175a969fa61dbcf4652a0f7c32cfc8
[submodule "oca/mass-mailing"]
path = oca/mass-mailing
url = git@github.com:OCA/mass-mailing.git
branch = 19.0
commit = 4baf365254fd80be51e99dd446de2d27e601b981
commit = 9d6b46cf3d7d0bae5b2b338d351b1d611a55d5c2
[submodule "oca/account-analytic"]
path = oca/account-analytic
url = git@github.com:OCA/account-analytic.git
branch = 19.0
commit = dc6afca735a01ed0c616339969a03fba9cef6102
commit = 4f78732271e81962c858f333e74b9b6690e6277c
[submodule "addons/website"]
path = addons/website
url = git@github.com:Mint-System/Odoo-Apps-Website
@@ -89,4 +89,4 @@
path = oca/hr-attendance
url = git@github.com:OCA/hr-attendance.git
branch = 19.0
commit = 729040b76e01f43dda5b3f171b581e5d87b6f590
commit = 6af354e9e0f21ebec6c9c052c2789578d33d206a
+1 -1
View File
@@ -1 +1 @@
3.13
3.12