From b939aafc0350efd94a1218dd7f56e0a7089b386e Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 03:13:39 -0400 Subject: [PATCH 01/41] [FIX] install s390x: the Debian build chain, package by package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bringing s390x up on Debian and Ubuntu turned up one blocker at a time, each found on a real machine: no wheel is published there, so everything compiles against distribution headers. manifold3d needs tbb, cmake and ninja; pymupdf loads libclang.so by its bare name through ctypes and only a versioned one is packaged; cryptography and bcrypt need Rust; pikepdf needs qpdf, whose -dev package is named differently across releases. Two traps cost the most time. One unknown name made apt refuse the whole batch of fifteen without ever saying which, so apt itself is now asked what is installable, and a failed batch retries package by package. And a failed unpack leaves dpkg half-configured, after which every apt call blames packages that are in fact installed -- hence the repair step. npm is the other half: npm@latest outran the packaged node and aborted install_os, NodeSource publishes nothing for s390x, and Ubuntu's nodejs package carries no npm on any release. --- FR --- Porter s390x sur Debian et Ubuntu a révélé un blocage à la fois, chacun sur une machine réelle : aucune roue n'y est publiée, donc tout se compile contre les en-têtes de la distribution. manifold3d exige tbb, cmake et ninja ; pymupdf charge libclang.so par son nom nu via ctypes et seul un nom versionné est empaqueté ; cryptography et bcrypt réclament Rust ; pikepdf veut qpdf, dont le paquet -dev change de nom selon la version. Deux pièges ont coûté le plus. Un seul nom inconnu faisait refuser à apt le lot entier de quinze, sans jamais dire lequel : on demande désormais à apt ce qui est installable, et un lot en échec reprend paquet par paquet. Et un dépaquetage raté laisse dpkg à moitié configuré, après quoi tout apt accuse des paquets pourtant installés — d'où l'étape de réparation. npm est l'autre moitié : npm@latest dépassait le node empaqueté et arrêtait install_os, NodeSource ne publie rien pour s390x, et le paquet nodejs d'Ubuntu ne porte npm sur aucune version. Assisted-by: Claude Opus 5 --- script/install/install_debian_dependency.sh | 251 +++++++++++++++++++- 1 file changed, 243 insertions(+), 8 deletions(-) diff --git a/script/install/install_debian_dependency.sh b/script/install/install_debian_dependency.sh index cc56946..e0022ea 100755 --- a/script/install/install_debian_dependency.sh +++ b/script/install/install_debian_dependency.sh @@ -58,7 +58,161 @@ fi #-------------------------------------------------- if [ "$(uname -m)" = "s390x" ]; then echo "Arch s390x detected" - sudo apt install rust-all libqpdf-dev libgeos-dev libproj-dev proj-bin proj-data libgeographiclib-dev freetds-dev freetds-bin libkrb5-dev libssl-dev pkg-config build-essential npm -y + # Sur une VM fraîche, l'index apt peut être vide : « apt install » échouerait + # alors sur TOUT le lot, et l'absence d'un seul paquet ne se voit que bien + # plus loin, sous la forme d'une commande introuvable. + ${APT_GET} update + # « ${APT_GET} » et non « sudo apt » : au 1er boot d'une image cloud, + # cloud-init tient le verrou apt, et un « apt install » nu échoue AUSSITÔT. + # Ces trois lots étaient les seuls du script à contourner le wrapper, et + # aucun ne vérifiait son résultat : libqpdf-dev manquait sans un mot, et + # l'échec ne se voyait qu'une heure plus tard, à la compilation de pikepdf. + # Un nom de paquet CHANGE d'une version d'Ubuntu à l'autre : GeographicLib + # s'appelle « libgeographic-dev » sur 20.04 et 22.04, « libgeographiclib-dev » + # depuis 24.04. Le nom récent existe pourtant dans la base apt des anciennes, + # SANS version installable — apt refuse alors le lot ENTIER, emportant les + # quatorze autres paquets. On demande donc à apt lui-même quel nom est + # réellement installable, seule autorité sur la question. + apt_pick() { + for candidate in "$@"; do + if apt-get install -s -qq "${candidate}" > /dev/null 2>&1; then + echo "${candidate}" + return 0 + fi + done + echo "$1" + } + # Vrai si dpkg considère le paquet installé et configuré. + apt_is_installed() { + [ "$(dpkg-query -W -f='${db:Status-Status}' "$1" 2>/dev/null)" = "installed" ] + } + # Le lot d'abord, pour la vitesse. S'il échoue, on reprend paquet par paquet + # afin de NOMMER le fautif : sinon un seul nom obsolète masque les autres et + # le message ne dit pas lequel corriger. + apt_install_batch() { + if ${APT_GET} install "$@" -y; then + return 0 + fi + # Un dépaquetage raté laisse dpkg à moitié configuré, et TOUT apt suivant + # rend « Unmet dependencies » — y compris pour des paquets déjà installés. + # Sans cette réparation, la boucle ci-dessous accusait les quinze paquets + # à cause d'un seul, et l'installation s'arrêtait sur un paquet présent. + echo "Lot apt en echec : tentative de reparation dpkg..." + sudo dpkg --configure -a || true + ${APT_GET} --fix-broken install -y || true + if ${APT_GET} install "$@" -y; then + echo "Reparation reussie." + return 0 + fi + echo "Reprise paquet par paquet pour identifier le fautif..." + df -h / | sed 's/^/ disque: /' + APT_FAILED="" + for one in "$@"; do + ${APT_GET} install "${one}" -y && continue + # L'échec peut n'être qu'un contrecoup de l'état global : on ne l'impute + # au paquet que si dpkg confirme qu'il n'est PAS installé. + apt_is_installed "${one}" || APT_FAILED="${APT_FAILED} ${one}" + done + [ -z "${APT_FAILED}" ] + } + APT_FAILED="" + GEO_DEV="$(apt_pick libgeographiclib-dev libgeographic-dev)" + # manifold3d (via to-3mf) et pymupdf n'ont pas de roue s390x : ils se + # compilent, d'où tbb, cmake et ninja. Un seul lot, une seule reprise. + apt_install_batch rust-all libqpdf-dev libgeos-dev libproj-dev proj-bin \ + proj-data "${GEO_DEV}" freetds-dev freetds-bin libkrb5-dev libssl-dev \ + pkg-config build-essential zlib1g-dev libjpeg-dev libtbb-dev cmake \ + ninja-build + if [[ -n "${APT_FAILED}" ]]; then + # Seul un paquet dont dépend la SUITE immédiate est bloquant. Les autres + # servent des modules Odoo optionnels : les rendre fatals immobiliserait + # toute l'installation pour un module que personne n'utilise ici. + echo "Paquets s390x non installables :${APT_FAILED}" + for essential in build-essential pkg-config libssl-dev zlib1g-dev \ + libjpeg-dev cmake; do + case " ${APT_FAILED} " in + *" ${essential} "*) + echo "apt-get s390x : '${essential}' est indispensable, arret." + exit 1 + ;; + esac + done + echo "Aucun n'est indispensable a la compilation : on continue." + fi + # pymupdf non plus n'a pas de roue s390x : il compile MuPDF, dont le + # générateur de liaisons charge « libclang.so » par son nom nu, via ctypes. + # La roue PyPI libclang, qui embarque cette bibliothèque, n'existe pas ici. + # Le paquet de la distribution la fournit bien dans le chemin de l'éditeur de + # liens, mais sous un nom versionné : il ne manque que le lien non versionné. + ${APT_GET} install libclang-dev -y + CLANG_LIB_DIR="/usr/lib/s390x-linux-gnu" + if [ ! -e "${CLANG_LIB_DIR}/libclang.so" ]; then + CLANG_SO="$(ls -1 ${CLANG_LIB_DIR}/libclang-[0-9]*.so 2>/dev/null | sort -V | tail -1)" + if [ -n "${CLANG_SO}" ]; then + sudo ln -s "${CLANG_SO}" "${CLANG_LIB_DIR}/libclang.so" + sudo ldconfig + echo "libclang.so -> ${CLANG_SO}" + else + echo "Attention : libclang introuvable, la compilation de pymupdf va echouer." + fi + fi + # pikepdf n'a pas de roue s390x et se lie à qpdf, dont il exige une version + # PRÉCISE : 12.2.0 au minimum pour la 10.x. Ubuntu en livre 9.1.1 (20.04), + # 10.6.3 (22.04) et 11.9.0 (24.04) — trop anciennes, et aucune version de + # pikepdf ne descend sous qpdf 11.5. 25.10 et 26.04 passent, d'où le partage + # observé. On complète donc par les sources sous ce seuil ; /usr/local est + # déjà dans les chemins par défaut de g++ et de l'éditeur de liens. + QPDF_MIN=12.2.0 + QPDF_VER=12.3.2 + qpdf_have="$(pkg-config --modversion libqpdf 2>/dev/null || echo 0)" + if [ "$(printf '%s\n%s\n' "${QPDF_MIN}" "${qpdf_have}" | sort -V | head -1)" != "${QPDF_MIN}" ]; then + echo "qpdf ${qpdf_have} < ${QPDF_MIN} requis par pikepdf : compilation de qpdf ${QPDF_VER} (long en emulation)." + QPDF_TMP="$(mktemp -d)" + if curl -fsSL --max-time 600 -o "${QPDF_TMP}/qpdf.tar.gz" \ + "https://github.com/qpdf/qpdf/releases/download/v${QPDF_VER}/qpdf-${QPDF_VER}.tar.gz" && + tar -xzf "${QPDF_TMP}/qpdf.tar.gz" -C "${QPDF_TMP}" && + cmake -S "${QPDF_TMP}/qpdf-${QPDF_VER}" -B "${QPDF_TMP}/build" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DBUILD_DOC=OFF -DBUILD_STATIC_LIBS=OFF && + cmake --build "${QPDF_TMP}/build" -j"$(nproc)" && + sudo cmake --install "${QPDF_TMP}/build"; then + sudo ldconfig + echo "qpdf $(PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --modversion libqpdf 2>/dev/null) installe dans /usr/local." + else + echo "Attention : compilation de qpdf echouee, pikepdf ne pourra pas se construire." + fi + rm -rf "${QPDF_TMP}" + fi + # cryptography ne publie aucune roue s390x : elle se compile, et son + # Cargo.lock est en version 4, que seul cargo >= 1.78 sait lire. Ubuntu 24.04 + # livre 1.75 et s'arrête sur « lock file version 4 requires + # -Znext-lockfile-bump » ; 25.10 et au-delà passent. On complète alors par + # rustup, en exposant la chaîne hors du shell de connexion : les étapes + # suivantes sont des processus distincts, qui ne liront ni ~/.bashrc ni + # ~/.cargo/env. + CARGO_MIN_MINOR=78 + cargo_ver="$(cargo --version 2>/dev/null | awk '{print $2}')" + cargo_major="${cargo_ver%%.*}" + cargo_rest="${cargo_ver#*.}" + cargo_minor="${cargo_rest%%.*}" + cargo_ok=0 + if [[ "${cargo_major}" =~ ^[0-9]+$ && "${cargo_minor}" =~ ^[0-9]+$ ]]; then + if [[ ${cargo_major} -gt 1 ]] || + [[ ${cargo_major} -eq 1 && ${cargo_minor} -ge ${CARGO_MIN_MINOR} ]]; then + cargo_ok=1 + fi + fi + if [ "${cargo_ok}" -ne 1 ]; then + echo "cargo trop ancien ($(cargo --version 2>/dev/null || echo absent)) pour cryptography : installation via rustup." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --profile minimal --default-toolchain stable + for bin in cargo rustc; do + if [ -x "${HOME}/.cargo/bin/${bin}" ]; then + sudo ln -sf "${HOME}/.cargo/bin/${bin}" "/usr/local/bin/${bin}" + fi + done + echo "cargo retenu : $(PATH=/usr/local/bin:$PATH cargo --version 2>/dev/null || echo absent)" + fi fi #-------------------------------------------------- @@ -156,16 +310,97 @@ else NODE_MAJOR=22 fi -echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list -${APT_GET} update -${APT_GET} install nodejs -y +# NodeSource ne publie que amd64, arm64 et armhf : vérifié, binary-s390x +# répond 404. Ajouter le dépôt sur une autre architecture n'apporte rien et +# fait échouer « apt update » sur un index introuvable. La distribution +# fournit alors nodejs elle-même — Ubuntu 26.04 s390x livre la 22.22.1, ce qui +# convient. +NODE_ARCH="$(dpkg --print-architecture)" +case "${NODE_ARCH}" in + amd64 | arm64 | armhf) + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list + ${APT_GET} update + ;; + *) + echo "NodeSource ne publie pas pour ${NODE_ARCH} : nodejs vient de la distribution." + sudo rm -f /etc/apt/sources.list.d/nodesource.list + ${APT_GET} update + # Le paquet « nodejs » de NodeSource embarque npm ; celui d'Ubuntu NON — + # vérifié : /usr/bin/npm est absent du paquet sur jammy, noble et questing. + # Sans cette ligne, tout ce qui suit tombe sur « npm: command not found ». + NODE_PKG="nodejs npm" + ;; +esac +${APT_GET} install ${NODE_PKG:-nodejs} -y -sudo npm install npm@latest -g -retVal=$? -if [[ $retVal -ne 0 ]]; then - echo "npm install npm lastest installation error." +node_major() { + node --version 2>/dev/null | sed -n 's/^v\([0-9]*\).*/\1/p' +} + +# Le node de la distribution suffit sur les versions récentes, pas sur les +# anciennes : focal livre la 10, jammy la 12, alors que « less » exige node 18 +# et « rtlcss » node 12. Là où NodeSource n'a rien à offrir, l'archive +# officielle de nodejs.org prend le relais — elle publie bien linux-s390x, +# vérifié dans son index. Rien n'est téléchargé si la distribution suffit. +NODE_MIN=18 +node_have="$(node_major)" +if [[ -n "${NODE_PKG}" && ! (${node_have:-0} -ge ${NODE_MIN}) ]]; then + echo "node $(node --version 2>/dev/null || echo absent) trop ancien (< ${NODE_MIN}) : archive officielle Node ${NODE_MAJOR}." + case "${NODE_ARCH}" in + # Node 22 publie linux-{x64,arm64,armv7l,ppc64le,s390x} — vérifié dans son + # index. Pas de riscv64 : cette architecture garde le node de sa + # distribution, et le message plus bas le dit. + s390x) NODE_DIST_ARCH=s390x ;; + ppc64el) NODE_DIST_ARCH=ppc64le ;; + *) NODE_DIST_ARCH="" ;; + esac + NODE_VER="$(curl -fsSL --max-time 60 https://nodejs.org/dist/index.json | + grep -o "\"version\":\"v${NODE_MAJOR}\.[0-9.]*\"" | head -1 | cut -d'"' -f4)" + if [[ -z "${NODE_DIST_ARCH}" || -z "${NODE_VER}" ]]; then + echo "Pas d'archive Node officielle pour ${NODE_ARCH} : on garde celle de la distribution." + else + NODE_TGZ="node-${NODE_VER}-linux-${NODE_DIST_ARCH}.tar.xz" + NODE_TMP="$(mktemp -d)" + if curl -fsSL --max-time 600 -o "${NODE_TMP}/${NODE_TGZ}" \ + "https://nodejs.org/dist/${NODE_VER}/${NODE_TGZ}" && + curl -fsSL --max-time 120 -o "${NODE_TMP}/SHASUMS256.txt" \ + "https://nodejs.org/dist/${NODE_VER}/SHASUMS256.txt" && + (cd "${NODE_TMP}" && grep " ${NODE_TGZ}\$" SHASUMS256.txt | sha256sum -c -); then + # Un « npm install npm@latest -g » antérieur a pu déposer un npm + # incompatible ici : l'archive écrase les fichiers mais ne supprime pas + # ceux qu'elle n'a pas, ce qui laisserait un mélange des deux versions. + sudo rm -rf /usr/local/lib/node_modules/npm + sudo tar -xJf "${NODE_TMP}/${NODE_TGZ}" -C /usr/local --strip-components=1 + hash -r + echo "node $(node --version) / npm $(npm --version) installés dans /usr/local." + else + echo "Telechargement de Node ${NODE_VER} impossible : on garde celle de la distribution." + fi + rm -rf "${NODE_TMP}" + fi +fi + +if ! command -v npm >/dev/null 2>&1; then + echo "npm est introuvable apres l'installation de nodejs (${NODE_ARCH})." exit 1 fi + +# « npm@latest » dépasse régulièrement le node en place : sur Ubuntu 26.04 +# s390x, npm 12 exige node ^22.22.2 alors que l'archive livre la 22.22.1 — un +# correctif d'écart, et EBADENGINE. Pire sur un node ancien : npm 6 n'applique +# PAS « engines », il se contente d'avertir, installe npm 12 par-dessus et le +# rend inutilisable (« Cannot find module 'node:path' », absent avant node +# 14.18). Le npm livré AVEC node est compatible par construction ; cette mise à +# niveau n'est qu'un confort, on ne la tente que si node est assez récent. +if [[ "$(node_major)" =~ ^[0-9]+$ && $(node_major) -ge ${NODE_MAJOR} ]]; then + sudo npm install npm@latest -g + retVal=$? + if [[ $retVal -ne 0 ]]; then + echo "Avertissement : npm n'a pas pu être mis à niveau, on garde $(npm --version)." + fi +else + echo "npm $(npm --version) conservé : node $(node --version) est en deçà de ${NODE_MAJOR}." +fi sudo npm install -g rtlcss retVal=$? if [[ $retVal -ne 0 ]]; then From 3fbde848f8ebdc6a7e7746ee8c15d6907c33b69f Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 05:02:11 -0400 Subject: [PATCH 02/41] [ADD] poetry: pin a dependency per architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit factur-x >= 4.0 needs saxonche, which Saxonica ships no s390x wheel for. Pin 3.x there, 4.2+ everywhere else. PEP 508 markers now survive the whole chain: kept instead of evaluated, and each line reaches "poetry add" as a single argument -- the "grep -v ';'" silently dropped them. Since "poetry add" dedupes by name, the multi-constraint form, which the CLI cannot express, is written back into the pyproject and the lock regenerated. PyMuPDF is set aside on s390x for the same reason: MuPDF does not build there. Along the way, the ignore list missed pymssql, whose key carried its version, and two pre-existing solver deadlocks, lxml-html-clean and s3fs, blocked any regeneration. --- FR --- factur-x >= 4.0 dépend de saxonche, sans roue s390x chez Saxonica. On épingle la 3.x là-bas, la 4.2+ ailleurs. Le marqueur PEP 508 traverse maintenant la chaîne : il est conservé au lieu d'être évalué, et chaque ligne devient un seul argument de « poetry add » — le « grep -v ';' » écartait ces lignes en silence. « poetry add » dédupliquant par nom, la forme multi-contraintes, hors de portée du CLI, est réécrite dans le pyproject, puis le lock refait. PyMuPDF est écarté sur s390x pour la même raison : MuPDF ne s'y construit pas. Au passage, la liste d'ignorés ratait pymssql, dont la clé portait sa version, et deux blocages préexistants du solveur, lxml-html-clean et s3fs, interdisaient toute régénération. Assisted-by: Claude Opus 5 --- .../poetry.odoo18.0_python3.12.10.lock | 2622 +++++++++++------ .../pyproject.odoo18.0_python3.12.10.toml | 100 +- .../requirements.odoo18.0_python3.12.10.txt | 21 +- script/poetry/poetry_add_build_dependency.sh | 25 +- script/poetry/poetry_update.py | 134 +- 5 files changed, 1954 insertions(+), 948 deletions(-) diff --git a/requirement/poetry.odoo18.0_python3.12.10.lock b/requirement/poetry.odoo18.0_python3.12.10.lock index 5045a11..63c3fb1 100644 --- a/requirement/poetry.odoo18.0_python3.12.10.lock +++ b/requirement/poetry.odoo18.0_python3.12.10.lock @@ -233,6 +233,62 @@ files = [ frozenlist = ">=1.1.0" typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} +[[package]] +name = "altcha" +version = "2.1.0" +description = "A library for creating and verifying challenges for ALTCHA." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "altcha-2.1.0-py3-none-any.whl", hash = "sha256:843de4146e14c604ceca7e563eb245e0cfb96936323ecac2fd898fab6e034cd2"}, + {file = "altcha-2.1.0.tar.gz", hash = "sha256:5d4d15d89f7833f0f32bb89a1dd650923d3cb113ee9f1178d8a69b2172d087ce"}, +] + +[[package]] +name = "anchorscad-linear" +version = "0.1.1" +description = "Linear algebra API for AnchorSCAD. Includes GMatrix, GVector, translate, rotate, angle and more." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "anchorscad_linear-0.1.1-py3-none-any.whl", hash = "sha256:304c3cc8c42c1eec6794589717007d1364832f5449533acd1ea409d303d52eab"}, + {file = "anchorscad_linear-0.1.1.tar.gz", hash = "sha256:075bec6bca4ba4d888614b6769cf686cd5ab60a22c2d360f2794697bc94a858a"}, +] + +[package.dependencies] +anchorscad-test-tools = "*" +numpy = "*" + +[[package]] +name = "anchorscad-test-tools" +version = "0.1.0" +description = "Test helpers for Anchorscad packages." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "anchorscad_test_tools-0.1.0-py3-none-any.whl", hash = "sha256:6d960acfa9829348cfb76456460ce9da1255b396eb127bcbc59358dadc990b8c"}, + {file = "anchorscad_test_tools-0.1.0.tar.gz", hash = "sha256:a4334b2a45e7171a86dc176c678e0ca657bc3fca2ab71873d939feea2f605399"}, +] + +[[package]] +name = "anchorscad-utils" +version = "0.1.2" +description = "Utility libraries for AnchorSCAD. Includes Colour, OpenSCAD finder, Process manager and more." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "anchorscad_utils-0.1.2-py3-none-any.whl", hash = "sha256:1441494ff592897863fe1208bf97c466730b5a0fe6cf58fe8c6f842fde6a66b2"}, + {file = "anchorscad_utils-0.1.2.tar.gz", hash = "sha256:2fc5a7726c52804db20e8d37382857e3e16ab10385256a4ddf20fe2a0fb521fc"}, +] + +[package.dependencies] +anchorscad-test-tools = "*" +numpy = "*" + [[package]] name = "annotated-doc" version = "0.0.4" @@ -380,13 +436,13 @@ joserfc = ">=1.6.0" [[package]] name = "avalara" -version = "26.4.0" +version = "26.7.3" description = "Avalara Tax Python SDK." optional = false python-versions = "*" groups = ["main"] files = [ - {file = "avalara-26.4.0.tar.gz", hash = "sha256:e9ef58256ba2eb858ab285b397ef0259a2ff070f6329262123c3a7ac0628d52a"}, + {file = "avalara-26.7.3.tar.gz", hash = "sha256:094ca08d567edbf55932bb86b9812aa488e0d44d44bb16029e4de1e788ebba71"}, ] [package.dependencies] @@ -639,16 +695,37 @@ urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version > [package.extras] crt = ["awscrt (==0.29.2)"] +[[package]] +name = "cachecontrol" +version = "0.14.4" +description = "httplib2 caching for requests" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b"}, + {file = "cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1"}, +] + +[package.dependencies] +msgpack = ">=0.5.2,<2.0.0" +requests = ">=2.16.0" + +[package.extras] +dev = ["cachecontrol[filecache,redis]", "cheroot (>=11.1.2)", "cherrypy", "codespell", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "types-redis", "types-requests"] +filecache = ["filelock (>=3.8.0)"] +redis = ["redis (>=2.10.5)"] + [[package]] name = "cachetools" -version = "7.1.1" +version = "7.1.7" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "cachetools-7.1.1-py3-none-any.whl", hash = "sha256:0335cd7a0952d2b22327441fb0628139e234c565559eeb91a8a4ac7551c5353d"}, - {file = "cachetools-7.1.1.tar.gz", hash = "sha256:27bdf856d68fd3c71c26c01b5edc312124ed427524d1ddb31aa2b7746fe20d4b"}, + {file = "cachetools-7.1.7-py3-none-any.whl", hash = "sha256:ef98ef375ad188819ef2f9b3645e3987f4b8c5b7550e436ad998c2de78296df0"}, + {file = "cachetools-7.1.7.tar.gz", hash = "sha256:a3e2a00b14d8f8a6b70c1dae7b4685e7ad3bc965c5b42124a2d6ce895da6cf50"}, ] [[package]] @@ -1069,6 +1146,18 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "cloudpickle" +version = "3.1.2" +description = "Pickler class to extend the standard pickle.Pickler functionality" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a"}, + {file = "cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414"}, +] + [[package]] name = "code-writer" version = "1.3.0" @@ -1297,14 +1386,14 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "cssselect" -version = "1.4.0" +version = "1.5.0" description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "cssselect-1.4.0-py3-none-any.whl", hash = "sha256:c0ec5c0191c8ee39fcc8afc1540331d8b55b0183478c50e9c8a79d44dbceb1d8"}, - {file = "cssselect-1.4.0.tar.gz", hash = "sha256:fdaf0a1425e17dfe8c5cf66191d211b357cf7872ae8afc4c6762ddd8ac47fc92"}, + {file = "cssselect-1.5.0-py3-none-any.whl", hash = "sha256:1d1aded98e82bdde447ded990a191fd6916177c4f0c914fb62eccd58e2ffcdcc"}, + {file = "cssselect-1.5.0.tar.gz", hash = "sha256:3cbe82dd7acbee9ba9e5723b5f9e4749826912f1fb31cd7f92aabed5fde15b15"}, ] [[package]] @@ -1369,63 +1458,111 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "cython" -version = "3.2.4" +version = "3.2.9" description = "The Cython compiler for writing C extensions in the Python language." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "cython-3.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb0cc0f23b9874ad262d7d2b9560aed9c7e2df07b49b920bda6f2cc9cb505e"}, - {file = "cython-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f136f379a4a54246facd0eb6f1ee15c3837cb314ce87b677582ec014db4c6845"}, - {file = "cython-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35ab0632186057406ec729374c737c37051d2eacad9d515d94e5a3b3e58a9b02"}, - {file = "cython-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ca2399dc75796b785f74fb85c938254fa10c80272004d573c455f9123eceed86"}, - {file = "cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed"}, - {file = "cython-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67922c9de058a0bfb72d2e75222c52d09395614108c68a76d9800f150296ddb3"}, - {file = "cython-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b362819d155fff1482575e804e43e3a8825332d32baa15245f4642022664a3f4"}, - {file = "cython-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a64a112a34ec719b47c01395647e54fb4cf088a511613f9a3a5196694e8e382"}, - {file = "cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9"}, - {file = "cython-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:869487ea41d004f8b92171f42271fbfadb1ec03bede3158705d16cd570d6b891"}, - {file = "cython-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55b6c44cd30821f0b25220ceba6fe636ede48981d2a41b9bbfe3c7902ce44ea7"}, - {file = "cython-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:767b143704bdd08a563153448955935844e53b852e54afdc552b43902ed1e235"}, - {file = "cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0"}, - {file = "cython-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03893c88299a2c868bb741ba6513357acd104e7c42265809fd58dce1456a36fc"}, - {file = "cython-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f81eda419b5ada7b197bbc3c5f4494090e3884521ffd75a3876c93fbf66c9ca8"}, - {file = "cython-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:83266c356c13c68ffe658b4905279c993d8a5337bb0160fa90c8a3e297ea9a2e"}, - {file = "cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa"}, - {file = "cython-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3b5ac54e95f034bc7fb07313996d27cbf71abc17b229b186c1540942d2dc28e"}, - {file = "cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f43be4eaa6afd58ce20d970bb1657a3627c44e1760630b82aa256ba74b4acb"}, - {file = "cython-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:983f9d2bb8a896e16fa68f2b37866ded35fa980195eefe62f764ddc5f9f5ef8e"}, - {file = "cython-3.2.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55eb425c0baf1c8a46aa4424bc35b709db22f3c8a1de33adb3ecb8a3d54ea42a"}, - {file = "cython-3.2.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f583cad7a7eed109f0babb5035e92d0c1260598f53add626a8568b57246b62c3"}, - {file = "cython-3.2.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:72e6c0bbd978e2678b45351395f6825b9b8466095402eae293f4f7a73e9a3e85"}, - {file = "cython-3.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:14dae483ca2838b287085ff98bc206abd7a597b7bb16939a092f8e84d9062842"}, - {file = "cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf"}, - {file = "cython-3.2.4-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d5267f22b6451eb1e2e1b88f6f78a2c9c8733a6ddefd4520d3968d26b824581"}, - {file = "cython-3.2.4-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b6e58f73a69230218d5381817850ce6d0da5bb7e87eb7d528c7027cbba40b06"}, - {file = "cython-3.2.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e71efb20048358a6b8ec604a0532961c50c067b5e63e345e2e359fff72feaee8"}, - {file = "cython-3.2.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:28b1e363b024c4b8dcf52ff68125e635cb9cb4b0ba997d628f25e32543a71103"}, - {file = "cython-3.2.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:31a90b4a2c47bb6d56baeb926948348ec968e932c1ae2c53239164e3e8880ccf"}, - {file = "cython-3.2.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e65e4773021f8dc8532010b4fbebe782c77f9a0817e93886e518c93bd6a44e9d"}, - {file = "cython-3.2.4-cp39-abi3-win32.whl", hash = "sha256:2b1f12c0e4798293d2754e73cd6f35fa5bbdf072bdc14bc6fc442c059ef2d290"}, - {file = "cython-3.2.4-cp39-abi3-win_arm64.whl", hash = "sha256:3b8e62049afef9da931d55de82d8f46c9a147313b69d5ff6af6e9121d545ce7a"}, - {file = "cython-3.2.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f8d685a70bce39acc1d62ec3916d9b724b5ef665b0ce25ae55e1c85ee09747fc"}, - {file = "cython-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca578c9cb872c7ecffbe14815dc4590a003bc13339e90b2633540c7e1a252839"}, - {file = "cython-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b84d4e3c875915545f77c88dba65ad3741afd2431e5cdee6c9a20cefe6905647"}, - {file = "cython-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:fdfdd753ad7e18e5092b413e9f542e8d28b8a08203126090e1c15f7783b7fe57"}, - {file = "cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c"}, - {file = "cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6"}, + {file = "cython-3.2.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2751b7c0cb13135aadcc1cc8fe223be98d458cd1132611d31675e768a5e4a2e9"}, + {file = "cython-3.2.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccb0bc6b8437cfcb04459a02bc5ecc58bf161ea11659438945ab5a1392ee39fe"}, + {file = "cython-3.2.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c2974742433be5c36ae1df1e761ce0063753fc2702316d5a3062b0a4e94a1df"}, + {file = "cython-3.2.9-cp310-cp310-win_amd64.whl", hash = "sha256:a3fc783d12202d1b064b6f125772d85f00e36e62eee6b2e415f56d8fd2d2e7b2"}, + {file = "cython-3.2.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e8ec2d5a7b798c84d6779e7ca5318fd928b8fe9dd021106d714dc2e77cdc7555"}, + {file = "cython-3.2.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cac792b0bde1c86d8f832e513cd061b7e682181cc5a6d843d487e6c8ae9d5fcb"}, + {file = "cython-3.2.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c843dd85857cd0d0da055489f448d032866120cfb094ce16205a1ff54d06353"}, + {file = "cython-3.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:efd54fe07f808e7e82f6a04f370457ca02e770c948f0e2f83345ccc7ec8bb829"}, + {file = "cython-3.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:114b2dee0fa1daa48a59574d848da0ff1b6bdb725a755e9b92fad14962e1ff8d"}, + {file = "cython-3.2.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5cd9c5f138cb052130b40ad3b6976d2180c35348410995812678f4636bd8f94"}, + {file = "cython-3.2.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23e80bc885c599e72072e18d0746df82d394b73100c1e153cda7359e6e59fe09"}, + {file = "cython-3.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b1fd5a9c03f72a18618668a8e90d569442ed742f910e3ad003dcc9348e9598b"}, + {file = "cython-3.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44b7fc417933d65bf31bce00337ff318efa3ea59daed21d10fe842d41e657c08"}, + {file = "cython-3.2.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41637c644d7224d2d3170ee312077c2173693f0d4a0da1c82a0cffa3680a42dc"}, + {file = "cython-3.2.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f815cd3387bd88ca9eeabc357ce4bdf884eef0ab2949db0fbd5e0b69fe5ee422"}, + {file = "cython-3.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:ddb43637b7749b88644df4319e1c36f767e7fb71a92bb9942558c8de2f1cc5f4"}, + {file = "cython-3.2.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63a42e131112072f912b66734088abc946c6bc42cb7454461a6dfdd2d09d3bae"}, + {file = "cython-3.2.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f25b402a6f1eed34af2af57603a9aadcf594dacaced1655e925eb317fba3f337"}, + {file = "cython-3.2.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d3cba9c4770cf76370fead8d2ae71bc474f3ef67ae4119e455d025726bcdc1c"}, + {file = "cython-3.2.9-cp314-cp314-win_amd64.whl", hash = "sha256:56d95c0674c25f281c6ae8f1d17bd425d6c2818bb304ff781831bb5d00d04b0b"}, + {file = "cython-3.2.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:788ea800618d20158166d09f60835aab50922ae1cfe3108c93fa72e173b7002d"}, + {file = "cython-3.2.9-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:522e277c29ebee304e73f3a78307549d09908b0c63098421a12dffc8d779fd74"}, + {file = "cython-3.2.9-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e9c39962f749bd40c6e8c5063d498bd3749947888306ffedb99a0cab3a69f995"}, + {file = "cython-3.2.9-cp38-cp38-win_amd64.whl", hash = "sha256:681980fcc82b346ac83bc7d59e68ca1eba74ed9576f5d8584761d0c6c68c45d0"}, + {file = "cython-3.2.9-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:944dc8747f640b3527649c566a5fc75ee0c15e80642ea2fdae4fe6378e1a9d4a"}, + {file = "cython-3.2.9-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4b871ad97dd7fb1cbf56f6238c54423febd310afc1d9d9bc70c69c89b7ce57fc"}, + {file = "cython-3.2.9-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9b6ebc6c74b4318eaa4e51e520dc8b95ebc7b262953c3ecb24131104681f14e"}, + {file = "cython-3.2.9-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:92989da161a7d18a7ad4baebc49289b2b77556d5a94916f90140ba26aecf6892"}, + {file = "cython-3.2.9-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e75ec625d8f8781ced690b7a2f5c2d138067711cf24bb8fb68c872c30c2fefe5"}, + {file = "cython-3.2.9-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:2b1756ddc3bc0cd4341a515fc420c3e25e13c249f5537159b3fb0bff8d19e55c"}, + {file = "cython-3.2.9-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7d41baea51ea00f9237f75af498577827493bca5e9b45bbd4e351543727e589a"}, + {file = "cython-3.2.9-cp39-abi3-win32.whl", hash = "sha256:61d4abbf84f77c8d19361d05d9f51d65d8d95e74f736eae55fa1aed8a1430469"}, + {file = "cython-3.2.9-cp39-abi3-win_arm64.whl", hash = "sha256:57a6a78d14f7dd7d6062d9bca694e2a8c1c14113b6ceceea076abcd1161fdc5a"}, + {file = "cython-3.2.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d15c4d4ba8a3ad284124384f769465bfba45512aaa4c81caea8a165f79fc042a"}, + {file = "cython-3.2.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2fe4bc1b8fa4130563b94a3614db9ea17e92a455bed00e6a5d03eaff434b33c"}, + {file = "cython-3.2.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c534d782757710e39f3c9582214d7a28a0d68c86e78ee3df1d729cbc270e133"}, + {file = "cython-3.2.9-cp39-cp39-win_amd64.whl", hash = "sha256:c6d4a92a87d238231564b5cbf27a8d1e46b7e62040c66b5ff33778a16b22295e"}, + {file = "cython-3.2.9-py3-none-any.whl", hash = "sha256:a2b0e87f6b80790c929308ca0831d686f7a180feab684fe8cd4a4380bd96aaca"}, + {file = "cython-3.2.9.tar.gz", hash = "sha256:d249c9022ab13286b17bd66f30609e800c5f95efeecb06168990c7a66cecde6c"}, ] +[[package]] +name = "dask" +version = "2026.7.1" +description = "Parallel PyData with Task Scheduling" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "dask-2026.7.1-py3-none-any.whl", hash = "sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6"}, + {file = "dask-2026.7.1.tar.gz", hash = "sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7"}, +] + +[package.dependencies] +click = ">=8.1" +cloudpickle = ">=3.0.0" +fsspec = ">=2021.09.0" +packaging = ">=20.0" +partd = ">=1.4.0" +pyyaml = ">=5.4.1" +toolz = ">=0.12.0" + +[package.extras] +array = ["numpy (>=1.24)"] +complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)"] +dataframe = ["dask[array]", "pandas (>=2.0)", "pyarrow (>=16.0)"] +diagnostics = ["bokeh (>=3.1.0)", "jinja2 (>=2.10.3)"] +distributed = ["distributed (>=2026.7.1,<2026.7.2)"] +test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] + +[[package]] +name = "datatrees" +version = "0.3.7" +description = "Wrapper for dataclasses with auto field injection, binding, self-default lamdas and more." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "datatrees-0.3.7-py3-none-any.whl", hash = "sha256:c0108a9d344af959c6fa5bf08ce0d246fc923aefd3accd120fecc5e36fa7011d"}, + {file = "datatrees-0.3.7.tar.gz", hash = "sha256:d15cc9e1666464660d71d99b311bed32e52d2973d0dc16006495da8969745411"}, +] + +[package.dependencies] +frozendict = ">=2.3.8" +sortedcollections = ">=2.1.0" + +[package.extras] +types = ["typing-extensions (>=4.1.0) ; python_version < \"3.11\""] + [[package]] name = "dateparser" -version = "1.4.0" +version = "1.4.2" description = "Date parsing library designed to parse dates from HTML pages" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "dateparser-1.4.0-py3-none-any.whl", hash = "sha256:7902b8e85d603494bf70a5a0b1decdddb2270b9c6e6b2bc8a57b93476c0df378"}, - {file = "dateparser-1.4.0.tar.gz", hash = "sha256:97a21840d5ecdf7630c584f673338a5afac5dfe84f647baf4d7e8df98f9354a4"}, + {file = "dateparser-1.4.2-py3-none-any.whl", hash = "sha256:752f3d49d477cf7f60a7a9c8bcb19c882496ede0e377d5a3d80014cdfeca7050"}, + {file = "dateparser-1.4.2.tar.gz", hash = "sha256:bed2a3fd9bad8f2fb2d72b57748bada260b3a9349a264c22ffc23c3249d7049a"}, ] [package.dependencies] @@ -1436,7 +1573,6 @@ tzlocal = ">=0.2" [package.extras] calendars = ["convertdate (>=2.2.1)", "hijridate"] -fasttext = ["fasttext (>=0.9.1)", "numpy (>=1.22.0,<2)"] langdetect = ["langdetect (>=1.0.0)"] [[package]] @@ -1509,6 +1645,34 @@ files = [ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] +[[package]] +name = "distributed" +version = "2026.7.1" +description = "Distributed scheduler for Dask" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "distributed-2026.7.1-py3-none-any.whl", hash = "sha256:64eaf4406a04ad26acdc5b8e32fad6bac522d07539225a530bab49610b0114a9"}, + {file = "distributed-2026.7.1.tar.gz", hash = "sha256:f7c48ab6961734874521574d75898466703bce93f22f0cff5872c7e4ac6425d4"}, +] + +[package.dependencies] +click = ">=8.0" +cloudpickle = ">=3.0.0" +dask = ">=2026.7.1,<2026.7.2" +jinja2 = ">=2.10.3" +locket = ">=1.0.0" +msgpack = ">=1.0.2" +packaging = ">=20.0" +psutil = ">=5.8.0" +pyyaml = ">=5.4.1" +sortedcontainers = ">=2.0.5" +tblib = ">=1.6.0,<3.2.0 || >3.2.0,<3.2.1 || >3.2.1" +toolz = ">=0.12.0" +tornado = ">=6.2.0" +zict = ">=3.0.0" + [[package]] name = "distro" version = "1.9.0" @@ -1567,21 +1731,24 @@ files = [ [[package]] name = "dropbox" -version = "12.0.2" +version = "12.2.1" description = "Official Dropbox API Client" optional = false -python-versions = "*" +python-versions = ">=3.11" groups = ["main"] files = [ - {file = "dropbox-12.0.2-py2-none-any.whl", hash = "sha256:4b8207a9f4afd33726ec886c0d223f4bbc42fe649b87718690a24704f5e24c0c"}, - {file = "dropbox-12.0.2-py3-none-any.whl", hash = "sha256:c5b7e9c2668adb6b12dcecd84342565dc50f7d35ab6a748d155cb79040979d1c"}, - {file = "dropbox-12.0.2.tar.gz", hash = "sha256:50057fd5ad5fcf047f542dfc6747a896e7ef982f1b5f8500daf51f3abd609962"}, + {file = "dropbox-12.2.1-py3-none-any.whl", hash = "sha256:a0f9cdc3505b104f7a14b35f81a8a0be6f44eb738475ef324243684c25efe1fb"}, + {file = "dropbox-12.2.1.tar.gz", hash = "sha256:61fcb821f7e8585aa4b9e2abc94801aaf8d65ed5ca89313450e56d51c46add7e"}, ] [package.dependencies] requests = ">=2.16.2" -six = ">=1.12.0" -stone = ">=2,<3.3.3" +stone = ">=3.5.3,<4" + +[package.extras] +dev = ["build", "flake8", "twine", "wheel"] +docs = ["sphinx", "sphinx_rtd_theme"] +test = ["coverage", "mock", "pytest", "pytest-mock", "stone (>=3.5.3,<4)"] [[package]] name = "easygui" @@ -1743,20 +1910,20 @@ test = ["coverage[toml]", "fastapi (>=0.111)", "httpx", "pytest"] [[package]] name = "extract-msg" -version = "0.55.0" +version = "0.56.0" description = "Extracts emails and attachments saved in Microsoft Outlook's .msg files" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "extract_msg-0.55.0-py3-none-any.whl", hash = "sha256:baf0cdee9a8d267b70c366bc57ceb03dbfa1e7ab2dca6824169a7fe623f0917c"}, - {file = "extract_msg-0.55.0.tar.gz", hash = "sha256:cf08283498c3dfcc7f894dad1579f52e3ced9fb76b865c2355cbe757af8a54e1"}, + {file = "extract_msg-0.56.0-py3-none-any.whl", hash = "sha256:16adab2254cf95d7efe39c174a5ce98e3a799b25be01228f23f122ea84b8580b"}, + {file = "extract_msg-0.56.0.tar.gz", hash = "sha256:378d57487fbb9c2f0f4559b9713f8158744cd0c7e76baec1bcc021c9f3b04c27"}, ] [package.dependencies] -beautifulsoup4 = ">=4.11.1,<4.14" +beautifulsoup4 = ">=4.11.1,<5" compressed-rtf = ">=1.0.6,<2" -ebcdic = ">=1.1.1,<2" +ebcdic = ">=1.1.1,<3" olefile = "0.47" red-black-tree-mod = ">=1.20,<=1.23" RTFDE = ">=0.1.1,<0.2" @@ -1796,52 +1963,52 @@ cli = ["requests"] [[package]] name = "ezdxf" -version = "1.4.3" +version = "1.4.4" description = "A Python package to create/manipulate DXF drawings." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "ezdxf-1.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0efe2f5de8d74d6279929b604cdc31ae965cadd582bed3ee62f89cfd276b2fe4"}, - {file = "ezdxf-1.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d04180aa8d3c7957d9c3af336f90f342949285fe5b9e518e71d259ceba5912d1"}, - {file = "ezdxf-1.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b8ce0bbe7fdf7dd08a4c16b88e62b14499ab25f1ce0b66dffe469d4245c57ea"}, - {file = "ezdxf-1.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df95c5806f53fe59afb207c756a720ca4bb842790666bc7d5fa3765a1f7bc9f9"}, - {file = "ezdxf-1.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f2cf7f904cb78390f6df8a374c2cfe304bb50fce215768eab0e94677c32a17"}, - {file = "ezdxf-1.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5e9afbfcbd6ada2c0388b9880a8fa6ba9ae68df68ab8bd6ad006e870a767f4ec"}, - {file = "ezdxf-1.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8dced9b3a2ec787b866ad3485a4d7a61fc0f12095a99c21597cf76e9d141486"}, - {file = "ezdxf-1.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:6b10ee63ebb2047a3409b34f7cb64c47db3613ede5bc7ba8673654f395bb4983"}, - {file = "ezdxf-1.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b9e816358a552e838d5cf99863aa8fdc34243e6f82a9485382a4240079af2479"}, - {file = "ezdxf-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58a53813dfd7c37095350a0e812247328a1fd88e9e5c09eb67c12b4b0b4b9dc0"}, - {file = "ezdxf-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eb40fc4912ff648b869be87f9002a81b7ace228f5caf2ee6677a889eeeaf41b6"}, - {file = "ezdxf-1.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c375eec1adfff910a4701b293192e89bbec221ea8d026ab005e2710e229ee58"}, - {file = "ezdxf-1.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f86db7aa4ee1103a0fdf565e6a710c9db13cdcf41c19f3f617c183e6959440b"}, - {file = "ezdxf-1.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1dee422c8b4b120a8039945b1d52a3b100d0e5bc503b7c7b452e4eeafaf8062"}, - {file = "ezdxf-1.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:10255f1ed6aef3cbbfec54955c4c7dde7b23596c39f75283963ceaafe85d042c"}, - {file = "ezdxf-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43952cd04d4779e49f9c5a21a450f5dfc4c516b235ae2f7b533ff5e84e6fb22f"}, - {file = "ezdxf-1.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a30f4062c2b6143581a62e0494cf6203f192795a06a21d7cd794cedc84003012"}, - {file = "ezdxf-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f50c39894c517f69604ea64e2f7d311bb47e75d22d9eefff5a7709c300d1294d"}, - {file = "ezdxf-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7ff3d6befadc6758ad9c16974e8242d7bf887ec4132dd34bd8b87662c960d132"}, - {file = "ezdxf-1.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aea2c146828de59eef2ef4a2e4788aa8d0440cd82eb3dbcbb8f17284e31b9d14"}, - {file = "ezdxf-1.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cef58f6d2d65b5aa523272cf07f65408af00661b445e15ed23891516c18d9f8b"}, - {file = "ezdxf-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4827eaf03f5f456cf638d310f03eeb95b71f7f4468638d10ec467b359115486c"}, - {file = "ezdxf-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:230f5283f3380454e0b066bd9b91de2bbda56c50490fd23767a1e4e6728284f9"}, - {file = "ezdxf-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:7a2ec122035e07ff7e68d01cafaf74b916e8c16c0e29aed1dfdc390e00b30467"}, - {file = "ezdxf-1.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d24bf4db3ad5bc10046a10f0ecf3de619e96cf3ed9afe42c44f00ec6661409c"}, - {file = "ezdxf-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:096b71bc9e1a9cd425648feaf4b9f560e3256a871f5bdb1be1f064820906c21a"}, - {file = "ezdxf-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c0afc6aa7938f959dd6985f42f2617eed77fa35da4cceaf07516fd9591cdccf"}, - {file = "ezdxf-1.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7cff30c9987eb250bc4d35e494606affb618727c3b660accd68b74709629a41"}, - {file = "ezdxf-1.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ccc79f0c375090ac406e3ae0e469451d1f636d1682c8b9a8bb305d92847a33"}, - {file = "ezdxf-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5949b8c7914feca043409f51209ad7711aa7f0a6254ea36bdf28478829b2fa7e"}, - {file = "ezdxf-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48c3737b786c81b49fbdc82cda75ea38cb133142d214616e190aac6cc3eb2649"}, - {file = "ezdxf-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:d95cd8dcc6e6051ff9adf6eaed0d055ac464abea23c2a71aaba701dcfe7f49de"}, - {file = "ezdxf-1.4.3-py3-none-any.whl", hash = "sha256:19e464aa4525dca3f1dabce165308de7ac262f1122b3c3986320cbec9e8ca6be"}, - {file = "ezdxf-1.4.3.tar.gz", hash = "sha256:403adf7ce305877f6c9f3c007fe2e5c5df504dfb797032122abedd7170176764"}, + {file = "ezdxf-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fbd70284ac9c39581e4973589d10afd941bc6c64c1cdd087ecd387ce7697bd50"}, + {file = "ezdxf-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4e9aecff9c34a62f21115bd98c0ab76d3448064a3c31f1f1df6e9c19dc78ea56"}, + {file = "ezdxf-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ff103b9ef0de6eca86a32506a4a0717ff05024450e9da3ea9a77a73f63e1ac2"}, + {file = "ezdxf-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39c1e7d77220654482c2c2ba82a171c50a40f2295f9f4d3cb72bbc909f1f2fcb"}, + {file = "ezdxf-1.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd67e30aafdbf64c74a09908c6648e20fe4e0c58fd0504e6ea4913f654708919"}, + {file = "ezdxf-1.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edbd0c0350361104834688034fecb97eefaff486c872f83809c71ec6988e0236"}, + {file = "ezdxf-1.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e110caf14288923afa771a2d5752b0916b9a9621c1abf60f3caf7b454f989cf6"}, + {file = "ezdxf-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:080d487ff8f9cdda79b4c7743683d103235cf831da6ef473edfa80170d1bc61d"}, + {file = "ezdxf-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:39aadab5bc7a05deddbd0db9b984c8a4bd28bd4c1ac2c5ee03f0d1b096272a88"}, + {file = "ezdxf-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2ed65b191113a82133ba47daa6ebdd540b487b17394b1e327e9511dde87ad924"}, + {file = "ezdxf-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c3c8f2c2b62fe5b6c9b62ab8257b697535d62b0c79acf3a5040bd95edfab451"}, + {file = "ezdxf-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d68d5ab9e1071319030da3af27590f34c38c8544a5ea638d9a4a800a96abb3c2"}, + {file = "ezdxf-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f75a4f2924ebdda0f5b2779ff2135ba92de2596c95a8fa9b1d9ebcabea1be41"}, + {file = "ezdxf-1.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0eec8a2095b645420a67560e99041c0d25afefd39eab5bff8c736fdb32211a75"}, + {file = "ezdxf-1.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d87a85052e92b9c28e54605dbad65f779a3b61e6288e992d6ef00df7bde4bed3"}, + {file = "ezdxf-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:473d68c8f2b04ace513efa290a1cd0d3a4a589cdff48124f015f0e51034668b2"}, + {file = "ezdxf-1.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d723d35d08223318c17080a32a05192a1449e544dee867c9d9c6f25998fac303"}, + {file = "ezdxf-1.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b0c73b46085521bdbd4a3e7d88998da7bec5713ba89bbb502fdeffe6b42fc7e"}, + {file = "ezdxf-1.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac3e83a70c1f866dd5114192a6fab6e75cc83b75e907bc08c5d94b98cce14cc0"}, + {file = "ezdxf-1.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ebe09717ab6636bf48780eabdab51e3ab2135f7963bb52452444b6d95e03d12"}, + {file = "ezdxf-1.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44af248af0860fe8b1a39ca40e1cd2fa8ac37b22142b14b20c6a0caf1b50f932"}, + {file = "ezdxf-1.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02a097794625e9fc948e637dfe07057e98167b4fe215357a59645759fef6db3f"}, + {file = "ezdxf-1.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f093bd2228f5782d483aed3bd89cdb74b672b5f90c677c4125cd236ee5649151"}, + {file = "ezdxf-1.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:3e518d9ed4dcbe525d57f14c25c5e592f82660a690fdec39b03b092e6cdc4771"}, + {file = "ezdxf-1.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:278a09a845d67a4f893aebe9ef8dcd8894ece9f255d7d8bb21719ffd902109b8"}, + {file = "ezdxf-1.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0a787f8ebcbdcb9798c6fcfe90c23afd1a0993c8d4302b6cce5d8f19ae052ae"}, + {file = "ezdxf-1.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c451d1c20f08b30c735ca4d650fee5b44147402f4b5662f8550053ed8a3009c2"}, + {file = "ezdxf-1.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3d9a6663993fc644751647fd7843057b51742ffac27c48ce28ea9f81239613"}, + {file = "ezdxf-1.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c022cf094d21ad3db68557aa83628e4152355e8a8bef0fffd4f71a9ea325df"}, + {file = "ezdxf-1.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3be59c2a3a93585412a2a37af29b3ad7574fa3e429e624924c3285d0573388b3"}, + {file = "ezdxf-1.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:44a9716860b59dddd49a3708c04ff4580ad1e7de92ac93bb3213d85fba5ac93f"}, + {file = "ezdxf-1.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:207eed544417464ffaf2570880d58a792ecd2534c9c2dede45e895f3200b77e6"}, + {file = "ezdxf-1.4.4-py3-none-any.whl", hash = "sha256:666edda631ba717270293b734f5d58dd97a1d1aba4787187f09d0cc584645865"}, + {file = "ezdxf-1.4.4.tar.gz", hash = "sha256:da5a5e0e6bdbb6656f9c017b47edc7eafceb419d61a2b5de64ffb344c168e593"}, ] [package.dependencies] fonttools = "*" numpy = "*" -pyparsing = ">=2.0.1" +pyparsing = ">=3.0.0" typing_extensions = ">=4.6.0" [package.extras] @@ -1852,31 +2019,53 @@ draw5 = ["Pillow", "PyMuPDF (>=1.20.0)", "PyQt5", "matplotlib"] [[package]] name = "factur-x" -version = "4.2" +version = "3.16" description = "Factur-X and Order-X: electronic invoicing and ordering standards" optional = false python-versions = ">=3.7" groups = ["main"] +markers = "platform_machine == \"s390x\"" files = [ - {file = "factur_x-4.2-py3-none-any.whl", hash = "sha256:13d5aab316d5a164120b281c74ec90bfd9e712ed448a640825645f4c90184ce8"}, - {file = "factur_x-4.2.tar.gz", hash = "sha256:20000395e3fd81eec8a9c94e6b9b12743ee4e52aa9d581d5b6e7f8d2ec46484b"}, + {file = "factur_x-3.16-py3-none-any.whl", hash = "sha256:7b9ecee862b8ce90e4688a02bc5fb9f223410e198acd97aebd86c68d0856d02b"}, + {file = "factur_x-3.16.tar.gz", hash = "sha256:04cc309325d928880911b8e1652890d360e704e87173aeb2b519d7dbbeaa0aa5"}, ] [package.dependencies] lxml = "*" pypdf = ">=5.3.0" -saxonche = "*" + +[[package]] +name = "factur-x" +version = "6.7" +description = "Factur-X and Order-X: electronic invoicing and ordering standards" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_machine != \"s390x\"" +files = [ + {file = "factur_x-6.7-py3-none-any.whl", hash = "sha256:1ada0fd0cf8c20be7c09501e99199a4acfc1185bf9d334d0a5a796e31d71a16b"}, + {file = "factur_x-6.7.tar.gz", hash = "sha256:5fc9f66b52bdfdc816c7737c3b02978a4f64472a22e1e9b3384a0fecfc99d7b7"}, +] + +[package.dependencies] +lxml = "*" +pypdf = ">=5.3.0" +python-stdnum = "*" +requests = "*" + +[package.extras] +test = ["coverage[toml]", "pytest"] [[package]] name = "fastapi" -version = "0.136.1" +version = "0.141.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f"}, - {file = "fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f"}, + {file = "fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3"}, + {file = "fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1"}, ] [package.dependencies] @@ -1887,9 +2076,9 @@ typing-extensions = ">=4.8.0" typing-inspection = ">=0.4.2" [package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "fastar (>=0.9.0)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] -standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.32)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.32)", "fastar (>=0.9.0)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.32)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "filelock" @@ -2071,6 +2260,107 @@ files = [ [package.dependencies] python-dateutil = ">=2.7" +[[package]] +name = "frozendict" +version = "2.4.7" +description = "A simple immutable dictionary" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "frozendict-2.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bd37c087a538944652363cfd77fb7abe8100cc1f48afea0b88b38bf0f469c3d2"}, + {file = "frozendict-2.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b96f224a5431889f04b2bc99c0e9abe285679464273ead83d7d7f2a15907d35"}, + {file = "frozendict-2.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5c1781f28c4bbb177644b3cb6d5cf7da59be374b02d91cdde68d1d5ef32e046b"}, + {file = "frozendict-2.4.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8a06f6c3d3b8d487226fdde93f621e04a54faecc5bf5d9b16497b8f9ead0ac3e"}, + {file = "frozendict-2.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b809d1c861436a75b2b015dbfd94f6154fa4e7cb0a70e389df1d5f6246b21d1e"}, + {file = "frozendict-2.4.7-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75eefdf257a84ea73d553eb80d0abbff0af4c9df62529e4600fd3f96ff17eeb3"}, + {file = "frozendict-2.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a4d2b27d8156922c9739dd2ff4f3934716e17cfd1cf6fb61aa17af7d378555e9"}, + {file = "frozendict-2.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ebd953c41408acfb8041ff9e6c3519c09988fb7e007df7ab6b56e229029d788"}, + {file = "frozendict-2.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c64d34b802912ee6d107936e970b90750385a1fdfd38d310098b2918ba4cbf2"}, + {file = "frozendict-2.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:294a7d7d51dd979021a8691b46aedf9bd4a594ce3ed33a4bdf0a712d6929d712"}, + {file = "frozendict-2.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f65d1b90e9ddc791ea82ef91a9ae0ab27ef6c0cfa88fadfa0e5ca5a22f8fa22f"}, + {file = "frozendict-2.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:82d5272d08451bcef6fb6235a0a04cf1816b6b6815cec76be5ace1de17e0c1a4"}, + {file = "frozendict-2.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5943c3f683d3f32036f6ca975e920e383d85add1857eee547742de9c1f283716"}, + {file = "frozendict-2.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88c6bea948da03087035bb9ca9625305d70e084aa33f11e17048cb7dda4ca293"}, + {file = "frozendict-2.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ffd1a9f9babec9119712e76a39397d8aa0d72ef8c4ccad917c6175d7e7f81b74"}, + {file = "frozendict-2.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0ff6f57854cc8aa8b30947ec005f9246d96e795a78b21441614e85d39b708822"}, + {file = "frozendict-2.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d774df483c12d6cba896eb9a1337bbc5ad3f564eb18cfaaee3e95fb4402f2a86"}, + {file = "frozendict-2.4.7-cp310-cp310-win32.whl", hash = "sha256:a10d38fa300f6bef230fae1fdb4bc98706b78c8a3a2f3140fde748469ef3cfe8"}, + {file = "frozendict-2.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:dd518f300e5eb6a8827bee380f2e1a31c01dc0af069b13abdecd4e5769bd8a97"}, + {file = "frozendict-2.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:3842cfc2d69df5b9978f2e881b7678a282dbdd6846b11b5159f910bc633cbe4f"}, + {file = "frozendict-2.4.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:735be62d757e1e7e496ccb6401efe82b473faa653e95eec0826cd7819a29a34c"}, + {file = "frozendict-2.4.7-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff8584e3bbdc5c1713cd016fbf4b88babfffd4e5e89b39020f2a208dd24c900"}, + {file = "frozendict-2.4.7-cp36-cp36m-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:91a06ee46b3e3ef3b237046b914c0c905eab9fdfeac677e9b51473b482e24c28"}, + {file = "frozendict-2.4.7-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd7ba56cf6340c732ecb78787c4e9600c4bd01372af7313ded21037126d33ec6"}, + {file = "frozendict-2.4.7-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1b4426457757c30ad86b57cdbcc0adaa328399f1ec3d231a0a2ce7447248987"}, + {file = "frozendict-2.4.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22d337c76b765cb7961d4ee47fe29f89e30921eb47bf856b14dc7641f4df3e5"}, + {file = "frozendict-2.4.7-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57134ef5df1dd32229c148c75a7b89245dbdb89966a155d6dfd4bda653e8c7af"}, + {file = "frozendict-2.4.7-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:c89617a784e1c24a31f5aa4809402f8072a26b64ddbc437897f6391ff69b0ee9"}, + {file = "frozendict-2.4.7-cp36-cp36m-musllinux_1_2_armv7l.whl", hash = "sha256:176dd384dfe1d0d79449e05f67764c57c6f0f3095378bf00deb33165d5d2df5b"}, + {file = "frozendict-2.4.7-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:b1a94e8935c69ae30043b465af496f447950f2c03660aee8657074084faae0b3"}, + {file = "frozendict-2.4.7-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:c570649ceccfa5e11ad9351e9009dc484c315a51a56aa02ced07ae97644bb7aa"}, + {file = "frozendict-2.4.7-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:e0d450c9d444befe2668bf9386ac2945a2f38152248d58f6b3feea63db59ba08"}, + {file = "frozendict-2.4.7-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7469912c1a04102457871ff675aebe600dbb7e79a6450a166cc8079b88f6ca79"}, + {file = "frozendict-2.4.7-cp36-cp36m-win32.whl", hash = "sha256:2808bab8e21887a8c106cca5f6f0ab5bda7ee81e159409a10f53d57542ccd99c"}, + {file = "frozendict-2.4.7-cp36-cp36m-win_amd64.whl", hash = "sha256:ca17ac727ffeeba6c46f5a88e0284a7cb1520fb03127645fcdd7041080adf849"}, + {file = "frozendict-2.4.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef11dd996208c5a96eab0683f7a17cb4b992948464d2498520efd75a10a2aac"}, + {file = "frozendict-2.4.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b960e700dc95faca7dd6919d0dce183ef89bfe01554d323cf5de7331a2e80f83"}, + {file = "frozendict-2.4.7-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fc43257a06e6117da6a8a0779243b974cdb9205fed82e32eb669f6746c75d27d"}, + {file = "frozendict-2.4.7-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ece525da7d0aa3eb56c3e479f30612028d545081c15450d67d771a303ee7d4c"}, + {file = "frozendict-2.4.7-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ddffe7c0b3be414f88185e212758989c65b497315781290eb029e2c1e1fd64e"}, + {file = "frozendict-2.4.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05dd27415f913cd11649009f53d97eb565ce7b76787d7869c4733738c10e8d27"}, + {file = "frozendict-2.4.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0664092614d2b9d0aa404731f33ad5459a54fe8dab9d1fd45aa714fa6de4d0ef"}, + {file = "frozendict-2.4.7-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:830d181781bb263c9fa430b81f82c867546f5dcb368e73931c8591f533a04afb"}, + {file = "frozendict-2.4.7-cp37-cp37m-musllinux_1_2_armv7l.whl", hash = "sha256:c93827e0854393cd904b927ceb529afc17776706f5b9e45c7eaf6a40b3fc7b25"}, + {file = "frozendict-2.4.7-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:6d30dbba6eb1497c695f3108c2c292807e7a237c67a1b9ff92c04e89969d22d1"}, + {file = "frozendict-2.4.7-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:ec846bde66b75d68518c7b24a0a46d09db0aee5a6aefd2209d9901faf6e9df21"}, + {file = "frozendict-2.4.7-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:1df8e22f7d24172c08434b10911f3971434bb5a59b4d1b0078ae33a623625294"}, + {file = "frozendict-2.4.7-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:39abe54264ae69a0b2e00fabdb5118604f36a5b927d33e7532cd594c5142ebf4"}, + {file = "frozendict-2.4.7-cp37-cp37m-win32.whl", hash = "sha256:d10c2ea7c90ba204cd053167ba214d0cdd00f3184c7b8d117a56d7fd2b0c6553"}, + {file = "frozendict-2.4.7-cp37-cp37m-win_amd64.whl", hash = "sha256:346a53640f15c1640a3503f60ba99df39e4ab174979f10db4304bbb378df5cbd"}, + {file = "frozendict-2.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cc520f3f4af14f456143a534d554175dbc0f0636ffd653e63675cd591862a9d9"}, + {file = "frozendict-2.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7fd0d0bd3a79e009dddbf5fedfd927ad495c218cd7b13a112d28a37e2079725c"}, + {file = "frozendict-2.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a404857e48d85a517bb5b974d740f8c4fccb25d8df98885f3a2a4d950870b845"}, + {file = "frozendict-2.4.7-cp38-cp38-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f42e2c25d3eee4ea3da88466f38ed0dce8c622a1a9d92572e5ee53b7a6bb9ef1"}, + {file = "frozendict-2.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a083e9ee7a1904e545a6307c7db1dd76200077520fcbf7a98d886f81b57dd7"}, + {file = "frozendict-2.4.7-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f556ea05d9c5f6dae50d57ce6234e4ab1fbf4551dd0d52b4fed6ef537d9f3d3c"}, + {file = "frozendict-2.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:739ee81e574f33b46f1e6d9312f3ec2c549bdd574a4ebb6bf106775c9d85ca7b"}, + {file = "frozendict-2.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:48ab42b01952bc11543577de9fe5d9ca7c41b35dda36326a07fb47d84b3d5f22"}, + {file = "frozendict-2.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34233deb8d09e798e874a6ac00b054d2e842164d982ebd43eb91b9f0a6a34876"}, + {file = "frozendict-2.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:76bd99f3508cb2ec87976f2e3fe7d92fb373a661cacffb863013d15e4cfaf0eb"}, + {file = "frozendict-2.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a265e95e7087f44b88a6d78a63ea95a2ca0eb0a21ab4f76047f4c164a8beb413"}, + {file = "frozendict-2.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1662f1b72b4f4a2ffdfdc4981ece275ca11f90244208ac1f1fc2c17fc9c9437a"}, + {file = "frozendict-2.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2e5d2c30f4a3fea83a14b0a5722f21c10de5c755ab5637c70de5eb60886d58cd"}, + {file = "frozendict-2.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2cf0a665bf2f1ce69d3cd8b6d3574b1d32ae00981a16fa1d255d2da8a2e44b7c"}, + {file = "frozendict-2.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:708382875c3cfe91be625dddcba03dee2dfdadbad2c431568a8c7f2f2af0bbee"}, + {file = "frozendict-2.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:7fe194f37052a8f45a1a8507e36229e28b79f3d21542ae55ea6a18c6a444f625"}, + {file = "frozendict-2.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d8930877a2dd40461968d9238d95c754e51b33ce7d2a45500f88ffeed5cb7202"}, + {file = "frozendict-2.4.7-cp38-cp38-win32.whl", hash = "sha256:6991469a889ee8a108fe5ed1b044447c7b7a07da9067e93c59cbfac8c1d625cf"}, + {file = "frozendict-2.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:ebae8f4a07372acfc3963fc8d68070cdaab70272c3dd836f057ebbe9b7d38643"}, + {file = "frozendict-2.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1c521ad3d747aa475e9040e231f5f1847c04423bae5571c010a9d969e6983c40"}, + {file = "frozendict-2.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70e655c3aa5f893807830f549a7275031a181dbebeaf74c461b51adc755d9335"}, + {file = "frozendict-2.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11d35075f979c96f528d74ccbf89322a7ef8211977dd566bc384985ebce689be"}, + {file = "frozendict-2.4.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d4d7ec24d3bfcfac3baf4dffd7fcea3fa8474b087ce32696232132064aa062cf"}, + {file = "frozendict-2.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5694417864875ca959932e3b98e2b7d5d27c75177bf510939d0da583712ddf58"}, + {file = "frozendict-2.4.7-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:57a754671c5746e11140363aa2f4e7a75c8607de6e85a2bf89dcd1daf51885a7"}, + {file = "frozendict-2.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:313e0e1d8b22b317aa1f7dd48aec8cbb0416ddd625addf7648a69148fcb9ccff"}, + {file = "frozendict-2.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:176a66094428b9fd66270927b9787e3b8b1c9505ef92723c7b0ef1923dbe3c4a"}, + {file = "frozendict-2.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de1fff2683d8af01299ec01eb21a24b6097ce92015fc1fbefa977cecf076a3fc"}, + {file = "frozendict-2.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:115a822ecd754574e11205e0880e9d61258d960863d6fd1b90883aa800f6d3b3"}, + {file = "frozendict-2.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:de8d2c98777ba266f5466e211778d4e3bd00635a207c54f6f7511d8613b86dd3"}, + {file = "frozendict-2.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1e307be0e1f26cbc9593f6bdad5238a1408a50f39f63c9c39eb93c7de5926767"}, + {file = "frozendict-2.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:78a55f320ca924545494ce153df02d4349156cd95dc4603c1f0e80c42c889249"}, + {file = "frozendict-2.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e89492dfcc4c27a718f8b5a4c8df1a2dec6c689718cccd70cb2ceba69ab8c642"}, + {file = "frozendict-2.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:1e801d62e35df24be2c6f7f43c114058712efa79a8549c289437754dad0207a3"}, + {file = "frozendict-2.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:3ed9e2f3547a59f4ef5c233614c6faa6221d33004cb615ae1c07ffc551cfe178"}, + {file = "frozendict-2.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad0448ed5569f0a9b9b010af9fb5b6d9bdc0b4b877a3ddb188396c4742e62284"}, + {file = "frozendict-2.4.7-cp39-cp39-win32.whl", hash = "sha256:eab9ef8a9268042e819de03079b984eb0894f05a7b63c4e5319b1cf1ef362ba7"}, + {file = "frozendict-2.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:8dfe2f4840b043436ee5bdd07b0fa5daecedf086e6957e7df050a56ab6db078d"}, + {file = "frozendict-2.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:cc2085926872a1b26deda4b81b2254d2e5d2cb2c4d7b327abe4c820b7c93f40b"}, + {file = "frozendict-2.4.7-py3-none-any.whl", hash = "sha256:972af65924ea25cf5b4d9326d549e69a9a4918d8a76a9d3a7cd174d98b237550"}, + {file = "frozendict-2.4.7.tar.gz", hash = "sha256:e478fb2a1391a56c8a6e10cc97c4a9002b410ecd1ac28c18d780661762e271bd"}, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -2213,18 +2503,18 @@ files = [ [[package]] name = "fsspec" -version = "2026.4.0" +version = "2026.7.0" description = "File-system specification" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2"}, - {file = "fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4"}, + {file = "fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279"}, + {file = "fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88"}, ] [package.dependencies] -s3fs = {version = ">2024.2.0", optional = true, markers = "extra == \"s3\""} +s3fs = {version = ">=2026.6.0", optional = true, markers = "extra == \"s3\""} [package.extras] abfs = ["adlfs"] @@ -2234,24 +2524,24 @@ dask = ["dask", "distributed"] dev = ["pre-commit", "ruff (>=0.5)"] doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs (>2024.2.0)", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs (>2024.2.0)", "smbprotocol", "tqdm"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs (>=2026.4.0)", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs (>=2026.6.0)", "smbprotocol", "tqdm"] fuse = ["fusepy"] -gcs = ["gcsfs (>2024.2.0)"] +gcs = ["gcsfs (>=2026.4.0)"] git = ["pygit2"] github = ["requests"] -gs = ["gcsfs"] +gs = ["gcsfs (>=2026.4.0)"] gui = ["panel"] hdfs = ["pyarrow (>=1)"] http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] libarchive = ["libarchive-c"] oci = ["ocifs"] -s3 = ["s3fs (>2024.2.0)"] +s3 = ["s3fs (>=2026.6.0)"] sftp = ["paramiko"] smb = ["smbprotocol"] ssh = ["paramiko"] test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] -test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "backports-zstd ; python_version < \"3.14\"", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas (<3.0.0)", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""] +test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "backports-zstd ; python_version < \"3.14\"", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs (>=2026.4.0)", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas (<3.0.0)", "panel", "paramiko", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "s3fs (>=2026.6.0)", "smbprotocol", "tqdm", "urllib3", "zarr (<3.2.0)", "zstandard ; python_version < \"3.14\""] tqdm = ["tqdm"] [[package]] @@ -2319,14 +2609,14 @@ requests = ">=2.9" [[package]] name = "geojson" -version = "3.2.0" +version = "3.3.0" description = "Python bindings and utilities for GeoJSON" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "geojson-3.2.0-py3-none-any.whl", hash = "sha256:69d14156469e13c79479672eafae7b37e2dcd19bdfd77b53f74fa8fe29910b52"}, - {file = "geojson-3.2.0.tar.gz", hash = "sha256:b860baba1e8c6f71f8f5f6e3949a694daccf40820fa8f138b3f712bd85804903"}, + {file = "geojson-3.3.0-py3-none-any.whl", hash = "sha256:a2d885187eeaa8b357600b3fcc9d963cb4300d1694196636dbd7eddc82fd0825"}, + {file = "geojson-3.3.0.tar.gz", hash = "sha256:92e83b9cb378a450b42f1207bb9b2a031f9fc89185f335153c44369b8b8b71fd"}, ] [[package]] @@ -2410,14 +2700,14 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.50" +version = "3.1.59" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9"}, - {file = "gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc"}, + {file = "gitpython-3.1.59-py3-none-any.whl", hash = "sha256:67a82f537384578643624c8b2c531938a9b82be431663e575dcf638526631d4c"}, + {file = "gitpython-3.1.59.tar.gz", hash = "sha256:0a1475cfdc38a5bfba1a3e9a4a9da52a39749ecec322b772915c019f94e5b7e4"}, ] [package.dependencies] @@ -2425,7 +2715,7 @@ gitdb = ">=4.0.1,<5" [package.extras] doc = ["sphinx (>=7.4.7,<8)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] +test = ["basedpyright (==1.39.9) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] [[package]] name = "giturlparse" @@ -2554,6 +2844,18 @@ files = [ hpack = ">=4.1,<5" hyperframe = ">=6.1,<7" +[[package]] +name = "hershey-fonts" +version = "2.1.0" +description = "Vector font package with built-in fonts and font rendering iterators" +optional = false +python-versions = ">=2.7" +groups = ["main"] +files = [ + {file = "Hershey Fonts-2.1.0.tar.gz", hash = "sha256:ebdb0550f9cf78fed2a4bba71b971f5ce5f98ce630da9da9eaa4079b10e8b671"}, + {file = "Hershey_Fonts-2.1.0-py2.py3-none-any.whl", hash = "sha256:e53ef56bdef231f10f0576a6570f49fcc8442e45115028bc52a7ade15d824581"}, +] + [[package]] name = "hpack" version = "4.1.0" @@ -2837,6 +3139,18 @@ files = [ colors = ["colorama"] plugins = ["setuptools"] +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + [[package]] name = "jinja2" version = "3.1.2" @@ -3230,6 +3544,18 @@ files = [ {file = "libsass-0.22.0.tar.gz", hash = "sha256:3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425"}, ] +[[package]] +name = "locket" +version = "1.0.0" +description = "File-based locks for Python on Linux and Windows" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3"}, + {file = "locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632"}, +] + [[package]] name = "lottie" version = "0.7.2" @@ -3444,22 +3770,22 @@ lxml = "*" [[package]] name = "mako" -version = "1.3.12" +version = "1.4.1" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9"}, - {file = "mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a"}, + {file = "mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617"}, + {file = "mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27"}, ] [package.dependencies] -MarkupSafe = ">=0.9.2" +MarkupSafe = ">=2.0" [package.extras] babel = ["Babel"] -lingua = ["lingua"] +lingua = ["lingua (>=4.16)"] testing = ["pytest"] [[package]] @@ -3480,16 +3806,173 @@ six = "*" [package.extras] restructuredtext = ["rst2ansi"] +[[package]] +name = "manifold3d" +version = "3.5.2" +description = "Library for geometric robustness" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "manifold3d-3.5.2-cp310-cp310-macosx_10_14_universal2.whl", hash = "sha256:f9e861da6798e232a5a37bcf0b1918fc4230b4f68c1a49f3d88be8367366f8a2"}, + {file = "manifold3d-3.5.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:62277ca41a8a3ed833b77234b033afca9b4aba415040fc75c40298e207609d4a"}, + {file = "manifold3d-3.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:362e53ae46b6554b82039b01fe15622b3f2f46e4d34b4b412d316bf15b4bc134"}, + {file = "manifold3d-3.5.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1849d351946bc4866877b9a5e1e7bfc7936a257594146237a591f2e781a1a5a6"}, + {file = "manifold3d-3.5.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b1112eddaae18378434f793511a7c5f8622852c0432cebf7ccf14e1e98cd588"}, + {file = "manifold3d-3.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:d411682a4564dc14ad982c9870d5b77ac6c5ae8b22decf5974f05b9ae92014e8"}, + {file = "manifold3d-3.5.2-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:996d4b6785228507a016f935a79e1f474d9bbc0eb980c2383fb1b4503ae8c519"}, + {file = "manifold3d-3.5.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:b26e8a1cdf47aa68cdf19d7936cd763543dd27b2e5701ce921ea15a508cdb3ce"}, + {file = "manifold3d-3.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3c29ec0e6eef44c9f51ec9e33c86430b34e263ec023ab7e78ed0d82e16d1acea"}, + {file = "manifold3d-3.5.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7774db9819a1c19e913b8e7efb12f8799ae0e49fa6dff9eece22cb14fabe4cfc"}, + {file = "manifold3d-3.5.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f00749d35b78384075d3245f7728ba36394d2cf69e9d27d2e0458aa0243ee922"}, + {file = "manifold3d-3.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:14f8a2c298d255c71f4a90ea26797ab9951eb4c91c941f97e4b9f60a08fecf2d"}, + {file = "manifold3d-3.5.2-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:d54ffba210bbbfee76047d10e3fe29c9b3cb4060c554cb0ab3ad9da08bbf68d0"}, + {file = "manifold3d-3.5.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:c4e33237f131a89f316ea8119efc9065e6fcd5cee01ca1ade3aae9b35cc7c038"}, + {file = "manifold3d-3.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2efc6b3523cc686cf25da086d8240c7e441858ad5da5bed30deffda787ff759f"}, + {file = "manifold3d-3.5.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5da0069130b41a234516dc2773945f54423d29408eb8222eb3d5f59e0bc5d1"}, + {file = "manifold3d-3.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11c8824c311507b4adf854abfb828528c5efc2c90a9a17fd7c36cc60efe3f382"}, + {file = "manifold3d-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:a129d7a09421dd5e246503006c0f39f7dc73933b4ba80f4eae5e267805aac72f"}, + {file = "manifold3d-3.5.2-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:ba8aadabb41bc5630df42a6eab9fa4d4c55e1321e5885133599145f6d66e5801"}, + {file = "manifold3d-3.5.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:ccda91f02b6a46ccdc06d00707740c13442c7d93f6f25fc73e1ab70697531569"}, + {file = "manifold3d-3.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:36e68dc76087aa214546d5aee3a1ad7956b6759aa24d07e864f15c11f6139b16"}, + {file = "manifold3d-3.5.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cedd81ddd636aacdea0fc20e1b0ea6e04880c7297b72a5dc67c49ef8373683d0"}, + {file = "manifold3d-3.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbfd7a445aeeb24d1e17749e742c637db69fd15f6005ace3fb1a8bfa52beae45"}, + {file = "manifold3d-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:87c812f45979775b9345b1112f630253f043cfbcc53252f4002712c6a48e54ae"}, + {file = "manifold3d-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:dab5be20f185f696d72c61dcfaec228c7ca5dd73a9ebe6872efcc4eca19eaae2"}, + {file = "manifold3d-3.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:257d7b8b8627345a4bad1962f24a9ce9dee24e06ad5fb6a2a5a3b588117c1088"}, + {file = "manifold3d-3.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d406164d8a2212cf9d7dab026eac506ec6b7ca4c9364ae2ed2ee61d6d44f5538"}, + {file = "manifold3d-3.5.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a44236a98d27104c46e3375552fcbf6a2b7e1dce054c0a8d4f75169dc89a406"}, + {file = "manifold3d-3.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d0fd4de568f2bb0f616ce3f6583abf44e72c3ecaed49570a21f2a35afc3eac"}, + {file = "manifold3d-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:771216b3c291bb2bf0a3aadb991ed8d27bfc02505740b478fad260cf846b4346"}, + {file = "manifold3d-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b2425f101db1c51162e105bfe518a827c19d9df477e8b5ccf7fb71540cab2eff"}, + {file = "manifold3d-3.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ee15355cb134d7fa7df10ff20f5a6e3407610bf43bf539738a164efc368f24aa"}, + {file = "manifold3d-3.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:10af126693c3da44bda9ff4dfc0e1426845e4e6c1afe953c2b23479cd0ac05ab"}, + {file = "manifold3d-3.5.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8555bc5c6a7ea73c3fe7bce2b42127232540f7201cbe8ef96bf58669ef551e5a"}, + {file = "manifold3d-3.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bff6005d78728dcd4808f40f3f9a21cf41dd76a157e6ceaefd76fdaff08f3b"}, + {file = "manifold3d-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e94adfc225f259aab8f9728a1bcbc102e8d58eb4dfe8c2ce161398666cb22dcb"}, + {file = "manifold3d-3.5.2-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:6bbbacdf03cf081e623cd44d12655d09a0e47a58ae0f86db9b7dc3076863a839"}, + {file = "manifold3d-3.5.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:51013ab57acf86e4161caacba41b18eb455eed632792757e6dba7d2040232174"}, + {file = "manifold3d-3.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f6c6b7f9623f8f865ba8ca0b4edc17170b3ecb243e9795a70e5697e095437b2"}, + {file = "manifold3d-3.5.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f3032d8f134524d6ba521887cac35a0d5a31f72d1b5e757e082b1c2f9cd7952"}, + {file = "manifold3d-3.5.2-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:815908a7c827cdee602ee94549a314f4d603021edf32437f372ae0c584b6c12e"}, + {file = "manifold3d-3.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:4b610563e6417251da59950d8397e3e09322488eb552e4ded676ae53cf746287"}, + {file = "manifold3d-3.5.2.tar.gz", hash = "sha256:8d445798ba5f86efae18e0dca6cb71823165f0887767a274d7c14ed63ab64648"}, +] + +[package.dependencies] +numpy = {version = ">=1.26.0b1", markers = "python_version >= \"3.12\""} + +[[package]] +name = "mapbox" +version = "0.18.1" +description = "A Python client for Mapbox services" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "mapbox-0.18.1-py2.py3-none-any.whl", hash = "sha256:e61be76f1594c0851ec2b7c1d713d4ae8a9c07bada5fa3648ffd718b908b470b"}, + {file = "mapbox-0.18.1.tar.gz", hash = "sha256:13c0774d8e4a4de07c224db00f04f6da21a40c1df7febda1791444d95c3d3c2b"}, +] + +[package.dependencies] +boto3 = ">=1.4" +cachecontrol = "*" +iso3166 = "*" +polyline = ">=1.3.1" +python-dateutil = ">=2.5.0" +requests = "*" +uritemplate = ">=2.0" + +[package.extras] +test = ["coveralls", "pytest (>=2.8.3)", "pytest-cov", "responses", "tox"] + +[[package]] +name = "mapbox-earcut" +version = "2.0.0" +description = "Python bindings for the mapbox earcut C++ polygon triangulation library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "mapbox_earcut-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:c9a1dab7529f8e54bdb377f908e56f1e2b9a7e27ed168c64d3c7c38ed04ac201"}, + {file = "mapbox_earcut-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e5953098ea198253c8a40e2f282ca5b04d50ec2b9661e20c4cd2b2be39f0bb0"}, + {file = "mapbox_earcut-2.0.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efe5fd5de409e3b6d13907e73f295c8f1d63bdb6b8ca155dde4c93865796eafe"}, + {file = "mapbox_earcut-2.0.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd04da6edbca1dd68ddbfac2398a95c763f35d7317fed227fde5b3aff1253b18"}, + {file = "mapbox_earcut-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bdb8881e857d6d9277df696e9cfb8749c00d6162021d9359cba9da58dfdd4f5"}, + {file = "mapbox_earcut-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6e2d1bf5af90d5857955775b4d8ea15b02e172f2a8f194bba50ff95f8ff3e80e"}, + {file = "mapbox_earcut-2.0.0-cp310-cp310-win32.whl", hash = "sha256:5b0aa63dd890d712343095b05eb7b60e071912ad3ced1fc4187d6a6a739677bc"}, + {file = "mapbox_earcut-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b1355f13af89ea815b32f59a5455db295c965d51ab501bde0459cddc010a7149"}, + {file = "mapbox_earcut-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:9b5040e79e3783295e99c90277f31c1cbaddd3335297275331995ba5680e3649"}, + {file = "mapbox_earcut-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf43baafec3ef1e967319d9b5da96bc6ddf3dbb204b6f3535275eda4b519a72"}, + {file = "mapbox_earcut-2.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a283531847f603dd9d69afb75b21bd009d385ca9485fcd3e5a7fa5db1ccd913"}, + {file = "mapbox_earcut-2.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab697676f4cec4572d4e941b7a3429a6687bf2ac6e8db3f3781024e3239ae3a0"}, + {file = "mapbox_earcut-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1bdac76e048f4299accf4eaf797079ddfc330442e7231c15535ed198100d6c5"}, + {file = "mapbox_earcut-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4a6945b23f859bef11ce3194303d17bd371c86b637e7029f81b1feaff3db3758"}, + {file = "mapbox_earcut-2.0.0-cp311-cp311-win32.whl", hash = "sha256:8e119524c29406afb5eaa15e933f297d35679293a3ca62ced22f97a14c484cb5"}, + {file = "mapbox_earcut-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:378bbbb3304e446023752db8f44ecd6e7ef965bcbda36541d2ae64442ba94254"}, + {file = "mapbox_earcut-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:6d249a431abd6bbff36f1fd0493247a86de962244cc4081b4d5050b02ed48fb1"}, + {file = "mapbox_earcut-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db55ce18e698bc9d90914ee7d4f8c3e4d23827456ece7c5d7a1ec91e90c7122b"}, + {file = "mapbox_earcut-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01dd6099d16123baf582a11b2bd1d59ce848498cf0cdca3812fd1f8b20ff33b7"}, + {file = "mapbox_earcut-2.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d5a098aae26a52282bc981a38e7bf6b889d2ea7442f2cd1903d2ba842f4ff07"}, + {file = "mapbox_earcut-2.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de35f241d0b9110ad9260f295acedd9d7cc0d7acfe30d36b1b3ee8419c2caba1"}, + {file = "mapbox_earcut-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cb63ab85e2e430c350f93e75c13f8b91cb8c8a045f3cd714c390b69a720368a"}, + {file = "mapbox_earcut-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb3c9f069fc3795306db87f8139f70c4f047532f897a3de05f54dc1faebc97f6"}, + {file = "mapbox_earcut-2.0.0-cp312-cp312-win32.whl", hash = "sha256:eb290e6676217707ed238dd55e07b0a8ca3ab928f6a27c4afefb2ff3af08d7cb"}, + {file = "mapbox_earcut-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ef5b3319a43375272ad2cad9333ed16e569b5102e32a4241451358897e6f6ee"}, + {file = "mapbox_earcut-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4a3706feb5cc8c782d8f68bb0110c8d551304043f680a87a54b0651a2c208c3"}, + {file = "mapbox_earcut-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:582329a81bd36cf0f82e443c395bcb8cfdb10caddafec76acaebac7c20bf1c31"}, + {file = "mapbox_earcut-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d2ac5f610b3e44a3a0c4df06b5552d503b4f1c2c409eeca20dbe05112bd60955"}, + {file = "mapbox_earcut-2.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58cc88513b87734b243d86f0d3fb87e96e0a78d9abd8fd615c55f766dd63f949"}, + {file = "mapbox_earcut-2.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40218d887798451932f3c335992834aa807c35cd497c6e0733470fdbd77f9521"}, + {file = "mapbox_earcut-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:39fa5cfa0e855b028ec9b0200c88ebfa252448f343ce2f67b6fc07fe1f22a3ae"}, + {file = "mapbox_earcut-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:476b558473b8a43f238d46e819bc0f830c427842ec5feb19e23b4dcac8ad2455"}, + {file = "mapbox_earcut-2.0.0-cp313-cp313-win32.whl", hash = "sha256:8c2d125c182acbc490b39503c0dec4f937bae180d0849a26bcea0ee4a76024bd"}, + {file = "mapbox_earcut-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e049e6a37c228d7a9cb2f54ae405aa21d35c5175d849530fb32064ddb38ad5ab"}, + {file = "mapbox_earcut-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:8a833d73d63d4b6291bbd8b4d2f551e87f663282cdc547ecbbd9b423849ee996"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad1dc141797037b7d4c9d8d2e52b9665b36294913a8ec31008b282d1a95b9bdc"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0f0f5c6f5ed8ffdce8efe6a003ba598089d0ee07eabd41868db183be50484f9f"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82cd92775f37fd1e4b8464c5e74a00e87130eecc55ee3df2492b8ca2bdf6ef3e"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:626ffc1310e0cc8910283e4ac3139e5fb0458f18f2c4874162f66159951933ff"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ea951d764a356cad95b23fef950d8aa3b44b933795ad09d977fea7d4dbe377c3"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:df1f217624abb5e02ecabcbd84369de970b8d8bc1e4e7c164c1cfcaddad76ca3"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314-win32.whl", hash = "sha256:6fa61307d38b50fc9bd5449c00dbae46d270a32b372c6fc3b8af4b85c85746e4"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:0da20ed3c81b240450118773bcedfac34e70a56998f66147222c46f4356fff67"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:847e74bd5878e4c64793dc100f9288f5443f87c55c3fe391fd90509029136ff6"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ddc9e7175fc903185c64afbbf91febee56b50787dd0962fce2bfb4f20cf80d27"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6dc8a7568066af9a858018d6d92b7e77e164578f9fcd79093f1cbe4ec203461b"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6abc5340edd9b433ab2dab2ee033082a199d5c51cce445124626c0040ec0d81b"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df7afdd8078a9aa28f469d9242531d304e09a4b14e514f048e021a949f3777b4"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a286f73e612a46cafd6d6c843365265090517af16823e2f37277c13cd8b6f09"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8d081fe1d00dc553e3e68c02fc395324aad0d8ed955f3ff59289264c9b21ace4"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:13049ca96431bbc7ef7fd7780dd1872209ca11a5c1977f7aa91a1b574a8af863"}, + {file = "mapbox_earcut-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ace78e4fdba3b8cbb7768d44d77a981698305862a07f94bbb6f5cc16659adb4"}, + {file = "mapbox_earcut-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:6717796a5923ae57b3f805db4b2876b5dc90d4dbabc0585de9b45fcda7fa33af"}, + {file = "mapbox_earcut-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:008bbf38abb0112736b2522fa68553a92a51829d3a578bc1cd7e7c7cf7f6be6c"}, + {file = "mapbox_earcut-2.0.0-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68582876cdc7c420e452a8eec9cdbbeaf52b652d46481524c3469f452758b1b0"}, + {file = "mapbox_earcut-2.0.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4a90f8bcebbc4eacb18270f42a3bb4d2b37394fc738469fa7454cdf3c0e9e9e"}, + {file = "mapbox_earcut-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5472c11973ea1d1f5cfa76987a3b0885019a16d25b9a4d67b645fb73cb35328"}, + {file = "mapbox_earcut-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1192327e316df76649776de344356f3ced1625722f4c3ea86955d3233aadbda5"}, + {file = "mapbox_earcut-2.0.0-cp39-cp39-win32.whl", hash = "sha256:dd9699f99b461c8f540ae451cc1a399014275a082c44fb39f8b2e727d0f2e0ec"}, + {file = "mapbox_earcut-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:f6bc47aa17b753400151983bd00c1a5e74bb2b577fb56631bece4c6e4fff50c8"}, + {file = "mapbox_earcut-2.0.0.tar.gz", hash = "sha256:81eab6b86cf99551deb698b98e3f7502c57900e5c479df15e1bdaf1a57f0f9d6"}, +] + +[package.dependencies] +numpy = "*" + +[package.extras] +dev = ["mypy", "pytest (>=8.0)"] + [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, ] [package.dependencies] @@ -3502,7 +3985,7 @@ linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins (>=0.5.0)"] profiling = ["gprof2dot"] rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] [[package]] name = "markdown2" @@ -3610,83 +4093,71 @@ files = [ [[package]] name = "matplotlib" -version = "3.10.9" +version = "3.11.1" description = "Python plotting package" optional = false -python-versions = ">=3.10" +python-versions = ">=3.11" groups = ["main"] files = [ - {file = "matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217"}, - {file = "matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b"}, - {file = "matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37"}, - {file = "matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294"}, - {file = "matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65"}, - {file = "matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda"}, - {file = "matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb"}, - {file = "matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb"}, - {file = "matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb"}, - {file = "matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9"}, - {file = "matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb"}, - {file = "matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f"}, - {file = "matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80"}, - {file = "matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1"}, - {file = "matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320"}, - {file = "matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285"}, - {file = "matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2"}, - {file = "matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf"}, - {file = "matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6"}, - {file = "matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42"}, - {file = "matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f"}, - {file = "matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e"}, - {file = "matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f"}, - {file = "matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838"}, - {file = "matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2"}, - {file = "matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921"}, - {file = "matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8"}, - {file = "matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9"}, - {file = "matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4"}, - {file = "matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc"}, - {file = "matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99"}, - {file = "matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d"}, - {file = "matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8"}, - {file = "matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38"}, - {file = "matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d"}, - {file = "matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f"}, - {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b"}, - {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2"}, - {file = "matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716"}, - {file = "matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f"}, - {file = "matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456"}, - {file = "matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe"}, - {file = "matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6"}, - {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c"}, - {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4"}, - {file = "matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf"}, - {file = "matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39"}, - {file = "matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c"}, - {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b"}, - {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f"}, - {file = "matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585"}, - {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20"}, - {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba"}, - {file = "matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4"}, - {file = "matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358"}, + {file = "matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2"}, + {file = "matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c"}, + {file = "matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83"}, + {file = "matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099"}, + {file = "matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407"}, + {file = "matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f"}, + {file = "matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18"}, + {file = "matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74"}, + {file = "matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b"}, + {file = "matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea"}, + {file = "matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472"}, + {file = "matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481"}, + {file = "matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f"}, + {file = "matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a"}, + {file = "matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191"}, + {file = "matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1"}, + {file = "matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3"}, + {file = "matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e"}, + {file = "matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f"}, + {file = "matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b"}, + {file = "matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2"}, + {file = "matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f"}, + {file = "matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1"}, + {file = "matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464"}, + {file = "matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf"}, + {file = "matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f"}, + {file = "matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78"}, + {file = "matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a"}, + {file = "matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3"}, + {file = "matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319"}, + {file = "matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f"}, + {file = "matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be"}, + {file = "matplotlib-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2"}, + {file = "matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6"}, + {file = "matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685"}, + {file = "matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae"}, + {file = "matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda"}, + {file = "matplotlib-3.11.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb"}, + {file = "matplotlib-3.11.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2"}, + {file = "matplotlib-3.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d"}, + {file = "matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb"}, + {file = "matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987"}, + {file = "matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741"}, + {file = "matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb"}, + {file = "matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99"}, + {file = "matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30"}, ] [package.dependencies] contourpy = ">=1.0.1" cycler = ">=0.10" -fonttools = ">=4.22.0" +fonttools = ">=4.28.2" kiwisolver = ">=1.3.1" -numpy = ">=1.23" +numpy = ">=1.25" packaging = ">=20.0" -pillow = ">=8" +pillow = ">=9" pyparsing = ">=3" python-dateutil = ">=2.7" -[package.extras] -dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7,<10)"] - [[package]] name = "maxminddb" version = "3.1.1" @@ -3879,16 +4350,92 @@ files = [ jinja2 = "*" matplotlib = "*" +[[package]] +name = "msgpack" +version = "1.2.1" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c"}, + {file = "msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce"}, + {file = "msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74"}, + {file = "msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb"}, + {file = "msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22"}, + {file = "msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f"}, + {file = "msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d"}, + {file = "msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8"}, + {file = "msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66"}, + {file = "msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35"}, + {file = "msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64"}, + {file = "msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac"}, + {file = "msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24"}, + {file = "msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07"}, + {file = "msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064"}, + {file = "msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6"}, + {file = "msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707"}, + {file = "msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9"}, + {file = "msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a"}, + {file = "msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d"}, + {file = "msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7"}, + {file = "msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb"}, + {file = "msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b"}, + {file = "msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7"}, + {file = "msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273"}, + {file = "msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c"}, + {file = "msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1"}, + {file = "msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2"}, + {file = "msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107"}, + {file = "msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647"}, +] + [[package]] name = "msgraphfs" -version = "0.4" +version = "0.5" description = "" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "msgraphfs-0.4-py3-none-any.whl", hash = "sha256:758c249d8894eeaf3192a1c193521ad2d8cb4cd68a17fcb7fa449a146ad2357a"}, - {file = "msgraphfs-0.4.tar.gz", hash = "sha256:f76231687e84a8fa48e789246c1d93a3c42400f6f4c222bd6fa77128728aa4db"}, + {file = "msgraphfs-0.5-py3-none-any.whl", hash = "sha256:a2b3888560abddc37c64a722fd527b98b1f3881fd390c8c7f1717be4b91631b8"}, + {file = "msgraphfs-0.5.tar.gz", hash = "sha256:446090f67b4d0a7e39fdb883ef18424da4cb735cc4279d8c9e0a39b8989132dc"}, ] [package.dependencies] @@ -4189,84 +4736,78 @@ docopt = ">=0.6.2" [[package]] name = "numpy" -version = "2.4.4" +version = "2.5.2" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.11" +python-versions = ">=3.12" groups = ["main"] files = [ - {file = "numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db"}, - {file = "numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0"}, - {file = "numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015"}, - {file = "numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40"}, - {file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d"}, - {file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502"}, - {file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd"}, - {file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5"}, - {file = "numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e"}, - {file = "numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e"}, - {file = "numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e"}, - {file = "numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b"}, - {file = "numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e"}, - {file = "numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842"}, - {file = "numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8"}, - {file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121"}, - {file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e"}, - {file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44"}, - {file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d"}, - {file = "numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827"}, - {file = "numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a"}, - {file = "numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec"}, - {file = "numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50"}, - {file = "numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115"}, - {file = "numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af"}, - {file = "numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c"}, - {file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103"}, - {file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83"}, - {file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed"}, - {file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959"}, - {file = "numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed"}, - {file = "numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf"}, - {file = "numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d"}, - {file = "numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5"}, - {file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7"}, - {file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93"}, - {file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e"}, - {file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40"}, - {file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e"}, - {file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392"}, - {file = "numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008"}, - {file = "numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8"}, - {file = "numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233"}, - {file = "numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0"}, - {file = "numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a"}, - {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a"}, - {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b"}, - {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a"}, - {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d"}, - {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252"}, - {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f"}, - {file = "numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc"}, - {file = "numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74"}, - {file = "numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb"}, - {file = "numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e"}, - {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113"}, - {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d"}, - {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d"}, - {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f"}, - {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0"}, - {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150"}, - {file = "numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871"}, - {file = "numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e"}, - {file = "numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7"}, - {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4"}, - {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e"}, - {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c"}, - {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3"}, - {file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7"}, - {file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f"}, - {file = "numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119"}, - {file = "numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0"}, + {file = "numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c"}, + {file = "numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03"}, + {file = "numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb"}, + {file = "numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414"}, + {file = "numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9"}, + {file = "numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8"}, + {file = "numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab"}, + {file = "numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7"}, + {file = "numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a"}, + {file = "numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4"}, + {file = "numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1"}, + {file = "numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15"}, + {file = "numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080"}, + {file = "numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740"}, + {file = "numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56"}, + {file = "numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3"}, + {file = "numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee"}, + {file = "numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59"}, + {file = "numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d"}, + {file = "numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4"}, + {file = "numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657"}, + {file = "numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2"}, + {file = "numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1"}, + {file = "numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8"}, + {file = "numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323"}, + {file = "numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e"}, + {file = "numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65"}, + {file = "numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee"}, + {file = "numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68"}, + {file = "numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb"}, + {file = "numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98"}, + {file = "numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d"}, + {file = "numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf"}, + {file = "numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1"}, + {file = "numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f"}, + {file = "numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf"}, + {file = "numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4"}, + {file = "numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce"}, + {file = "numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26"}, + {file = "numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac"}, + {file = "numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba"}, + {file = "numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884"}, + {file = "numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e"}, + {file = "numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f"}, + {file = "numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842"}, + {file = "numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6"}, + {file = "numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8"}, + {file = "numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69"}, + {file = "numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab"}, + {file = "numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514"}, + {file = "numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710"}, + {file = "numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617"}, + {file = "numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7"}, + {file = "numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788"}, + {file = "numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b"}, + {file = "numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e"}, + {file = "numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a"}, + {file = "numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8"}, + {file = "numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc"}, + {file = "numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec"}, + {file = "numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0"}, + {file = "numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2"}, + {file = "numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251"}, + {file = "numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12"}, + {file = "numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e"}, + {file = "numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860"}, ] [[package]] @@ -4286,23 +4827,27 @@ numpy = ">=1.15" [[package]] name = "numpy-stl" -version = "3.2.0" +version = "4.0.0" description = "Library to make reading, writing and modifying both binary and ascii STL files easy." optional = false -python-versions = ">3.9.0" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "numpy_stl-3.2.0-py3-none-any.whl", hash = "sha256:697c81b107231362460aaedf4647e81ba54f54f59c896772fea7904c9c439da5"}, - {file = "numpy_stl-3.2.0.tar.gz", hash = "sha256:5a20c3f79cddaa0abc6a4b99f5486aceed4f88152f29b19a57acc844e183fd4d"}, + {file = "numpy_stl-4.0.0-py3-none-any.whl", hash = "sha256:8ee1413d12699c8a97821dc5d5f105d047c1aeecc3869dfcf1d92b50272da59c"}, + {file = "numpy_stl-4.0.0.tar.gz", hash = "sha256:086579c101fb2df9fb2594db7074728345710c931265cfdae4a50ed8d59d2f7e"}, ] [package.dependencies] -numpy = "*" +numpy = ">=1.24" python-utils = ">=3.4.5" +typing-extensions = ">=4.1" [package.extras] -docs = ["mock", "python-utils", "sphinx"] -tests = ["Sphinx", "cov-core", "coverage", "cython", "docutils", "execnet", "flake8", "numpy", "pep8", "py", "pyflakes", "pytest", "pytest-cache", "pytest-cov", "python-utils", "wheel"] +dev = ["basedpyright (>=1.0)", "mypy (>=1.0)", "numpy-stl[tests]", "pyrefly (>=0.1)", "ruff (>=0.15.0)"] +docs = ["furo (>=2025.1)", "sphinx (>=8.0) ; python_full_version < \"3.11.0\"", "sphinx (>=9.0) ; python_full_version >= \"3.11.0\""] +fast = ["speedups (>=2.0.0)"] +tests = ["coverage (>=7.0)", "pytest (>=9.0)", "pytest-cov (>=7.0)"] +tox = ["tox (>=4.0)", "tox-gh-actions (>=3.0)", "tox-uv (>=1.0)"] [[package]] name = "oauthlib" @@ -4321,6 +4866,18 @@ rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] +[[package]] +name = "odoo-test-helper" +version = "2.1.3" +description = "Our Odoo project tools" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "odoo_test_helper-2.1.3-py3-none-any.whl", hash = "sha256:8c40c8a01c7fcb59d8fbfcdbee83412bdaac3e24700f70daefa43ddd4b4146a0"}, + {file = "odoo_test_helper-2.1.3.tar.gz", hash = "sha256:58ab156fb4d4f10dce680a0043e4d0c86d60bbfaaaa53f425e30564d2f15f043"}, +] + [[package]] name = "odoorpc" version = "0.10.1" @@ -4405,14 +4962,14 @@ pydantic = ">=2.9" [[package]] name = "openai" -version = "2.34.0" +version = "2.53.0" description = "The official Python library for the openai API" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "openai-2.34.0-py3-none-any.whl", hash = "sha256:c996a71b1a210f3569844572ad4c609307e978515fb76877cf449b72596e549e"}, - {file = "openai-2.34.0.tar.gz", hash = "sha256:828b4efcbb126352c2b5eb97d33ae890c92a71ab72511aefc1b7fe64aeccb07b"}, + {file = "openai-2.53.0-py3-none-any.whl", hash = "sha256:c694ffc747a3c4d1663ef2b07b811315a476164ee5efa3a993967349ebca7618"}, + {file = "openai-2.53.0.tar.gz", hash = "sha256:baf5802ad08980e1d9d561e1b996e800c8bcd14af5847c6d0e7a5cc59e4d4116"}, ] [package.dependencies] @@ -4426,8 +4983,10 @@ tqdm = ">4" typing-extensions = ">=4.14,<5" [package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"] +aiohttp = ["aiohttp (>=3.14.1)", "httpx-aiohttp (>=0.1.9)"] +bedrock = ["botocore (>=1.40.0,<2)"] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] +httpx2 = ["anyio (>=4.10.0,<5)", "httpx (>=0.25.1,<1)", "httpx2 (>=2.7.0,<3)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] @@ -4448,14 +5007,14 @@ et-xmlfile = "*" [[package]] name = "openupgradelib" -version = "3.13.1" +version = "3.13.6" description = "A library with support functions to be called from Odoo migration scripts." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" groups = ["main"] files = [ - {file = "openupgradelib-3.13.1-py2.py3-none-any.whl", hash = "sha256:e7aa6bfe32f6c719bb122eef05e4a0e44515fe994be97d52f7127d9622a98816"}, - {file = "openupgradelib-3.13.1.tar.gz", hash = "sha256:3507c8043c1193f84b2713f132ed4725a06148766eeb142f0c34733d1952ec18"}, + {file = "openupgradelib-3.13.6-py2.py3-none-any.whl", hash = "sha256:5ac0d57279e73f13a80491b6dbc73ee25e8547f685cde9208ce995a8140d64e1"}, + {file = "openupgradelib-3.13.6.tar.gz", hash = "sha256:9cdf9be52427955ac83aa0f10b5bfb8425cbf3d63888e56a74420f39bdfc9a3f"}, ] [package.dependencies] @@ -4484,86 +5043,86 @@ test = ["pytest (>=7.0.0)", "pytest-cov (>=4.0.0)", "pytest-mock (>=3.0.0)"] [[package]] name = "orjson" -version = "3.11.8" +version = "3.11.9" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "orjson-3.11.8-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e6693ff90018600c72fd18d3d22fa438be26076cd3c823da5f63f7bab28c11cb"}, - {file = "orjson-3.11.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93de06bc920854552493c81f1f729fab7213b7db4b8195355db5fda02c7d1363"}, - {file = "orjson-3.11.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13"}, - {file = "orjson-3.11.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97d823831105c01f6c8029faf297633dbeb30271892bd430e9c24ceae3734744"}, - {file = "orjson-3.11.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60c0423f15abb6cf78f56dff00168a1b582f7a1c23f114036e2bfc697814d5f"}, - {file = "orjson-3.11.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01928d0476b216ad2201823b0a74000440360cef4fed1912d297b8d84718f277"}, - {file = "orjson-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4a639049c44d36a6d1ae0f4a94b271605c745aee5647fa8ffaabcdc01b69a6"}, - {file = "orjson-3.11.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3222adff1e1ff0dce93c16146b93063a7793de6c43d52309ae321234cdaf0f4d"}, - {file = "orjson-3.11.8-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3223665349bbfb68da234acd9846955b1a0808cbe5520ff634bf253a4407009b"}, - {file = "orjson-3.11.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:61c9d357a59465736022d5d9ba06687afb7611dfb581a9d2129b77a6fcf78e59"}, - {file = "orjson-3.11.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58fb9b17b4472c7b1dcf1a54583629e62e23779b2331052f09a9249edf81675b"}, - {file = "orjson-3.11.8-cp310-cp310-win32.whl", hash = "sha256:b43dc2a391981d36c42fa57747a49dae793ef1d2e43898b197925b5534abd10a"}, - {file = "orjson-3.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:c98121237fea2f679480765abd566f7713185897f35c9e6c2add7e3a9900eb61"}, - {file = "orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34"}, - {file = "orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8"}, - {file = "orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8"}, - {file = "orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4"}, - {file = "orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f"}, - {file = "orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c"}, - {file = "orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a"}, - {file = "orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c"}, - {file = "orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8"}, - {file = "orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6"}, - {file = "orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054"}, - {file = "orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7"}, - {file = "orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac"}, - {file = "orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06"}, - {file = "orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd"}, - {file = "orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f"}, - {file = "orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6"}, - {file = "orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8"}, - {file = "orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813"}, - {file = "orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec"}, - {file = "orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546"}, - {file = "orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506"}, - {file = "orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f"}, - {file = "orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e"}, - {file = "orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb"}, - {file = "orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942"}, - {file = "orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25"}, - {file = "orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2"}, - {file = "orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6"}, - {file = "orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d"}, - {file = "orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc"}, - {file = "orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559"}, - {file = "orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623"}, - {file = "orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c"}, - {file = "orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f"}, - {file = "orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55"}, - {file = "orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137"}, - {file = "orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53"}, - {file = "orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e"}, - {file = "orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6"}, - {file = "orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6"}, - {file = "orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b"}, - {file = "orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d"}, - {file = "orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8"}, - {file = "orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e"}, - {file = "orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4"}, - {file = "orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625"}, - {file = "orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5"}, - {file = "orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db"}, - {file = "orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b"}, - {file = "orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d"}, - {file = "orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858"}, - {file = "orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f"}, - {file = "orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d"}, - {file = "orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc"}, - {file = "orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf"}, - {file = "orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600"}, - {file = "orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade"}, - {file = "orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca"}, - {file = "orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817"}, - {file = "orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e"}, + {file = "orjson-3.11.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47"}, + {file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d"}, + {file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13"}, + {file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92"}, + {file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48"}, + {file = "orjson-3.11.9-cp310-cp310-win32.whl", hash = "sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94"}, + {file = "orjson-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244"}, + {file = "orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f"}, + {file = "orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1"}, + {file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd"}, + {file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff"}, + {file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980"}, + {file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2"}, + {file = "orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180"}, + {file = "orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02"}, + {file = "orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697"}, + {file = "orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49"}, + {file = "orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61"}, + {file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2"}, + {file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206"}, + {file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f"}, + {file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa"}, + {file = "orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470"}, + {file = "orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be"}, + {file = "orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624"}, + {file = "orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021"}, + {file = "orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218"}, + {file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9"}, + {file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677"}, + {file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4"}, + {file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0"}, + {file = "orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32"}, + {file = "orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979"}, + {file = "orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254"}, + {file = "orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e"}, + {file = "orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db"}, + {file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b"}, + {file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972"}, + {file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0"}, + {file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586"}, + {file = "orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673"}, + {file = "orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b"}, + {file = "orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9"}, + {file = "orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f"}, ] [[package]] @@ -4727,6 +5286,25 @@ files = [ {file = "parse_accept_language-0.1.2-py2.py3-none-any.whl", hash = "sha256:6708254304b09f1ea659a256ebd575f0f39310743d13b2c9b4a51e41a5047a49"}, ] +[[package]] +name = "partd" +version = "1.4.2" +description = "Appendable key-value storage" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f"}, + {file = "partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c"}, +] + +[package.dependencies] +locket = "*" +toolz = "*" + +[package.extras] +complete = ["blosc", "numpy (>=1.20.0)", "pandas (>=1.3)", "pyzmq"] + [[package]] name = "passlib" version = "1.7.4" @@ -4829,14 +5407,14 @@ ptyprocess = ">=0.5" [[package]] name = "phonenumbers" -version = "9.0.29" +version = "9.0.36" description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." optional = false python-versions = ">=2.5" groups = ["main"] files = [ - {file = "phonenumbers-9.0.29-py2.py3-none-any.whl", hash = "sha256:0eb26e0c578aa2d56428330d5b4a0a07dac9a452c4387bf76a76a88b1e839ab7"}, - {file = "phonenumbers-9.0.29.tar.gz", hash = "sha256:66455e5c5ca4197aca5a4217d0dc82717a22038292cfa322d5d5e7c46934a214"}, + {file = "phonenumbers-9.0.36-py2.py3-none-any.whl", hash = "sha256:c16a5b95178345ec2df1954578a4ac09e937443b2ee992dd7b15a8e8c81f4acf"}, + {file = "phonenumbers-9.0.36.tar.gz", hash = "sha256:60ca2a6870d2532d6e960105790e85e9d69270ede81746d10cf57f5e437b93ec"}, ] [[package]] @@ -5008,13 +5586,13 @@ xmp = ["defusedxml"] [[package]] name = "plaid-python" -version = "39.2.0" +version = "42.0.0" description = "Python client library for the Plaid API and Link" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "plaid_python-39.2.0.tar.gz", hash = "sha256:552aff15f8e4dbbf469fcab224465fa2359e68757b60df9c8ed40e8163aa6c20"}, + {file = "plaid_python-42.0.0.tar.gz", hash = "sha256:dfe00c8a68d80d4f4110a3a03a9dc8b0470f8c63dace0650f779032deaccf749"}, ] [package.dependencies] @@ -5036,14 +5614,14 @@ files = [ [[package]] name = "plotly" -version = "6.7.0" +version = "6.9.0" description = "An open-source interactive data visualization library for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "plotly-6.7.0-py3-none-any.whl", hash = "sha256:ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0"}, - {file = "plotly-6.7.0.tar.gz", hash = "sha256:45eea0ff27e2a23ccd62776f77eb43aa1ca03df4192b76036e380bb479b892c6"}, + {file = "plotly-6.9.0-py3-none-any.whl", hash = "sha256:36bebe2f1bb13884774fe61689c329071446f6ce4a8927fb1f0d6fb24f581236"}, + {file = "plotly-6.9.0.tar.gz", hash = "sha256:967ad33e8c704fed051800d11d985eb206a9c795c14206b30a6f463ed9c67d0d"}, ] [package.dependencies] @@ -5051,12 +5629,15 @@ narwhals = ">=1.15.1" packaging = "*" [package.extras] -dev = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.1.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +dev = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.3.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] dev-build = ["build", "jupyterlab", "pytest", "requests", "ruff (==0.11.12)"] dev-core = ["pytest", "requests", "ruff (==0.11.12)"] -dev-optional = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.1.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +dev-optional = ["anywidget", "build", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "jupyterlab", "kaleido (>=1.3.0)", "numpy (>=1.22)", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "polars[timezone]", "pyarrow", "pyshp", "pytest", "pytz", "requests", "ruff (==0.11.12)", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +dev-pandas1 = ["numpy (>=1,<2)", "pandas (>=1,<2)", "setuptools (<82)"] +dev-pandas2 = ["pandas (>=2,<3)"] +dev-pandas3 = ["pandas (>=3) ; python_version >= \"3.11\""] express = ["numpy (>=1.22)"] -kaleido = ["kaleido (>=1.1.0)"] +kaleido = ["kaleido (>=1.3.0)"] [[package]] name = "ply" @@ -5082,16 +5663,32 @@ files = [ {file = "polib-1.1.1.tar.gz", hash = "sha256:e02c355ae5e054912e3b0d16febc56510eff7e49d60bf22aecb463bd2f2a2dfa"}, ] +[[package]] +name = "polyline" +version = "2.0.4" +description = "A Python implementation of Google's Encoded Polyline Algorithm Format." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "polyline-2.0.4-py3-none-any.whl", hash = "sha256:a4e0c15b8ecb32915559f8cf210f1f8c2f5cc53d3cd32c91d7c1668d6e936e10"}, + {file = "polyline-2.0.4.tar.gz", hash = "sha256:f05ade694522bf1720febebe1672f820f43a13c6a1664751e7769d47e8ca9b1b"}, +] + +[package.extras] +dev = ["pylint (>=3.0.3,<3.1.0)", "pytest (>=7.0,<8.0)", "pytest-cov (>=4.0,<5.0)", "sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.0,<1.3.0)", "standard-imghdr ; python_version > \"3.12\"", "tomli ; python_version < \"3.11\""] +publish = ["build (>=0.8,<1.0)", "twine (>=6.0,<7.0)"] + [[package]] name = "pre-commit" -version = "4.6.0" +version = "4.6.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b"}, - {file = "pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9"}, + {file = "pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e"}, + {file = "pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441"}, ] [package.dependencies] @@ -5126,14 +5723,14 @@ test = ["mock", "nose"] [[package]] name = "prometheus-client" -version = "0.25.0" +version = "0.26.0" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1"}, - {file = "prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28"}, + {file = "prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6"}, + {file = "prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b"}, ] [package.extras] @@ -5490,19 +6087,19 @@ files = [ [[package]] name = "pydantic" -version = "2.13.3" +version = "2.13.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927"}, - {file = "pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d"}, + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.46.3" +pydantic-core = "2.46.4" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -5512,132 +6109,132 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.46.3" +version = "2.46.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1"}, - {file = "pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f"}, - {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3"}, - {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a"}, - {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807"}, - {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda"}, - {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57"}, - {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045"}, - {file = "pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943"}, - {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f"}, - {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4"}, - {file = "pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a"}, - {file = "pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7"}, - {file = "pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6"}, - {file = "pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5"}, - {file = "pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c"}, - {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e"}, - {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287"}, - {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe"}, - {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050"}, - {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2"}, - {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa"}, - {file = "pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c"}, - {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf"}, - {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b"}, - {file = "pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e"}, - {file = "pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb"}, - {file = "pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346"}, - {file = "pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6"}, - {file = "pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67"}, - {file = "pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089"}, - {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0"}, - {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789"}, - {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d"}, - {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c"}, - {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395"}, - {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396"}, - {file = "pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d"}, - {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca"}, - {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976"}, - {file = "pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b"}, - {file = "pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4"}, - {file = "pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1"}, - {file = "pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72"}, - {file = "pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37"}, - {file = "pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f"}, - {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8"}, - {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad"}, - {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c"}, - {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f"}, - {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35"}, - {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687"}, - {file = "pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3"}, - {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022"}, - {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23"}, - {file = "pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7"}, - {file = "pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13"}, - {file = "pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0"}, - {file = "pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec"}, - {file = "pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b"}, - {file = "pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018"}, - {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34"}, - {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7"}, - {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2"}, - {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba"}, - {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f"}, - {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22"}, - {file = "pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f"}, - {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127"}, - {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c"}, - {file = "pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1"}, - {file = "pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505"}, - {file = "pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e"}, - {file = "pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8"}, - {file = "pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374"}, - {file = "pydantic_core-2.46.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fa3eb7c2995aa443687a825bc30395c8521b7c6ec201966e55debfd1128bcceb"}, - {file = "pydantic_core-2.46.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d08782c4045f90724b44c95d35ebec0d67edb8a957a2ac81d5a8e4b8a200495"}, - {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:831eb19aa789a97356979e94c981e5667759301fb708d1c0d5adf1bc0098b873"}, - {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4335e87c7afa436a0dfa899e138d57a72f8aad542e2cf19c36fb428461caabd0"}, - {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99421e7684a60f7f3550a1d159ade5fdff1954baedb6bdd407cba6a307c9f27d"}, - {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd81f6907932ebac3abbe41378dac64b2380db1287e2aa64d8d88f78d170f51a"}, - {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f247596366f4221af52beddd65af1218797771d6989bc891a0b86ccaa019168"}, - {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:6dff8cc884679df229ebc6d8eb2321ea6f8e091bc7d4886d4dc2e0e71452843c"}, - {file = "pydantic_core-2.46.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68ef2f623dda6d5a9067ac014e406c020c780b2a358930a7e5c1b73702900720"}, - {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d56bdb4af1767cc15b0386b3c581fdfe659bb9ee4a4f776e92c1cd9d074000d6"}, - {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:91249bcb7c165c2fb2a2f852dbc5c91636e2e218e75d96dfdd517e4078e173dd"}, - {file = "pydantic_core-2.46.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b068543bdb707f5d935dab765d99227aa2545ef2820935f2e5dd801795c7dbd"}, - {file = "pydantic_core-2.46.3-cp39-cp39-win32.whl", hash = "sha256:dcda6583921c05a40533f982321532f2d8db29326c7b95c4026941fa5074bd79"}, - {file = "pydantic_core-2.46.3-cp39-cp39-win_amd64.whl", hash = "sha256:a35cc284c8dd7edae8a31533713b4d2467dfe7c4f1b5587dd4031f28f90d1d13"}, - {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46"}, - {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874"}, - {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76"}, - {file = "pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531"}, - {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803"}, - {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3"}, - {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5"}, - {file = "pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4"}, - {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25"}, - {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3"}, - {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536"}, - {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1"}, - {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c"}, - {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85"}, - {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8"}, - {file = "pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff"}, - {file = "pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, ] [package.dependencies] @@ -5706,6 +6303,87 @@ requests = ">=2.14.0" typing-extensions = ">=4.5.0" urllib3 = ">=1.26.0" +[[package]] +name = "pyglm" +version = "2.8.3" +description = "OpenGL Mathematics library for Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pyglm-2.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:159547399d3e700cdf2e6b6480c467f0a2e1abf140c01d395b7228793bda46ad"}, + {file = "pyglm-2.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3986e7c3b9d0b112972cc2dc2f02b6720cea16aca6ec38eee5bb9effa17af035"}, + {file = "pyglm-2.8.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c804ac7a678db3441f15f09cb600ed23c504ceab0d8129fc8818699074f816ba"}, + {file = "pyglm-2.8.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b24bd5f9ba4b6966a4bf7d38931cdf8db3a94cafac40e85fc64ec245f2289efa"}, + {file = "pyglm-2.8.3-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:463113f24a6b41eff41721c4c65013092368cf56ec32e0523b71a3df43d34cbf"}, + {file = "pyglm-2.8.3-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:a5d9e0fe8778cc16a18fdf594e4f672f3e18cac4b38c74d226e29badf0a3844d"}, + {file = "pyglm-2.8.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:19b8d942bf9ae5349e78c851094407af3c0ee967684dff11fd13954380fe48ca"}, + {file = "pyglm-2.8.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a9f9e0ab479a51d5482a3b1b0eec8b28cfd3436d9b213ecb71c706c0eb9da45f"}, + {file = "pyglm-2.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:f6f906e9e135b8a7f8548e31500a26bf2de94c507feace6f1a999d57774fa2b2"}, + {file = "pyglm-2.8.3-cp310-cp310-win_arm64.whl", hash = "sha256:638e7cd772d2a3cc0f1cc64904085b6eaf202d2790605a04e7c1bd559f06e99b"}, + {file = "pyglm-2.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94c7eb9c967423a123306f3a1bf66691cd66ed5ef17165c29424c096a2f96537"}, + {file = "pyglm-2.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43ac57b7ec33f0ec1e7224a4a8833c0f1405c446177e5abcd6d87867d286f8b3"}, + {file = "pyglm-2.8.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ccdc19eb1b432297aebc60e8f72f7870186f2defdaf2e0d0a5bc449057dad01"}, + {file = "pyglm-2.8.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2f1424cd0a5d49bc0d8c43a9028a471dcb90a22ba1954baad9158659726f256c"}, + {file = "pyglm-2.8.3-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:32e7a2815f39a49f5434d66d2d109c020d3e7125044905d3afebcb597d6a583e"}, + {file = "pyglm-2.8.3-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:96da922be7d7d754f2ea269687c9215906ecc651b5f197ff92dcb497230d5f57"}, + {file = "pyglm-2.8.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71132521c65b43badb3338edc12102eafacc4bf8597378ea7bb57a700975ce77"}, + {file = "pyglm-2.8.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:957816050c3bded151be3183e3160e7fc6b31f708b17d282f6c401ba1536f2a4"}, + {file = "pyglm-2.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae0dd576e89638654079f554d3f41b5d463ff6fee68961e4cd8b23069bff7e55"}, + {file = "pyglm-2.8.3-cp311-cp311-win_arm64.whl", hash = "sha256:28dba29d5b80cd9e81cf9875290bcedb451c83ca532de90c451d5b3a79727fcc"}, + {file = "pyglm-2.8.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72c75bd76ade851bf5c2b12cbe384193ea901a73474d57e4d7044bbbcd7d08fd"}, + {file = "pyglm-2.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:570da7032ff1185c842c5864d080ee5eab091fa53ec217b8d4d4034c22ba744f"}, + {file = "pyglm-2.8.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a14f1ffdc30a6f53334d16651888f60a2f8a63e05613357234c5592747b906bf"}, + {file = "pyglm-2.8.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d2305b258bebc358e00b8c85c3bc2d6f78d64f9b3f8802e1699c3e8dae54361"}, + {file = "pyglm-2.8.3-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:f80d208e558a024a3dc83300bd661a71aa4cda884a256c5b87179059a1c55373"}, + {file = "pyglm-2.8.3-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8abf221111db95e34620e446a853d43c887ced10d5c217e2df18471c9ac683ef"}, + {file = "pyglm-2.8.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71ace7b15dd6d2ea9dbeadd43738ce69e7134547fc4674617084df5096e9b866"}, + {file = "pyglm-2.8.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:04be7de24547130b8d413bc96ab9998d95db89cbaf224f401624ac1fa62bc431"}, + {file = "pyglm-2.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:5f7ff6f4ac14b092d897931b9ef76bb8108b5faa6f5118963c9de86c6fa18efc"}, + {file = "pyglm-2.8.3-cp312-cp312-win_arm64.whl", hash = "sha256:ba2de300dbaf39cb6cd82ce7db05a8dcf85bb7dacf1ec65db18b1ebc34438a0d"}, + {file = "pyglm-2.8.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a6d07b6b73e55a36e1b6daa63acb53f4912f2ddf88f434af4cc70441435aef8"}, + {file = "pyglm-2.8.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eaf6241f7c3ff169575e11da78f4439422517e42558961332db4cd09e9599267"}, + {file = "pyglm-2.8.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e1ec0dfb8f2c848c4ee6330c70a1ca9333004776c8e5ec76096e0b67c739f688"}, + {file = "pyglm-2.8.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3eea9093210afb946769c84fe31f17a9f73696a1161ee84fb42e9255f8c5cc8a"}, + {file = "pyglm-2.8.3-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:0c652650912dbd88994fa02ec9dba2f3b35dc3995427b6ae8056ae542d5a060a"}, + {file = "pyglm-2.8.3-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:e3bdda68f75ad270e66b0fc7a1883749489cca14c44cbe22bac038fa170a8a1c"}, + {file = "pyglm-2.8.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08f8b1bd0d80ce396ee9cd5d3d4c7aeb4bbfa2a54dc73413924e3c9982412528"}, + {file = "pyglm-2.8.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12fa61feefa5a255097d0887415bce9dd72c1dd5e5a8c6577121ee5348336a16"}, + {file = "pyglm-2.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:33118ef1d678ab573546757dee7f0a1ca2fba8e8d7760c9fe6320fe0cfa3deb7"}, + {file = "pyglm-2.8.3-cp313-cp313-win_arm64.whl", hash = "sha256:73ff3785dfc4ce017626d7ab56d6711a7119c29e2e71294efed73810c1d307f9"}, + {file = "pyglm-2.8.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b312012458d0537b3d84f24f4ba51fd3930df7a0773fc643e36f8df27b807c7a"}, + {file = "pyglm-2.8.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3249bfe5352e18cc777fac591665679a99f270e1ff27cd11dc349af07684f007"}, + {file = "pyglm-2.8.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb8ea0d6721c763a26eacde59b2c9165719050dcf49c99a2857f6e1e5a5f30bb"}, + {file = "pyglm-2.8.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:83effb89e2cf6dd79cf9ebecf2f9fbda3d25a92b61af264de3cadad408911de6"}, + {file = "pyglm-2.8.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:e958d65ed55f2716fd8a3a2ef872cc52893ea7300d7feec62dccb27ec25fbc2f"}, + {file = "pyglm-2.8.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:ec7cc14d2eb9f46a18012ee7c1a164e0395b058ceb6e341bf6d986316b698574"}, + {file = "pyglm-2.8.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4beb0ada21e7641a577f274496451befcde79965467ef4027bd933334b3de39b"}, + {file = "pyglm-2.8.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77878c1b8c713b9e39fe32f870d82e20c864da3b11a3b875eca06c270b04cdbe"}, + {file = "pyglm-2.8.3-cp314-cp314-win_amd64.whl", hash = "sha256:15c77bc46ff69d945565309e13ca99c4a001d6a941a80c45f26fbdec80fa16c4"}, + {file = "pyglm-2.8.3-cp314-cp314-win_arm64.whl", hash = "sha256:2b16ec33bd43c514502bae8de2b319d168259090e101e1cde79cd0a7d33e1185"}, + {file = "pyglm-2.8.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6a1f1ab8debc06e0fdedb3f4285ded4bec38bf075652c393039838504767e6cf"}, + {file = "pyglm-2.8.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a7835d18747ab9f8e736e343cc35bee0a514f18add282f1fc8035945fcf9d9bd"}, + {file = "pyglm-2.8.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:af3ddee3d150bbef68ee7338ccd3e0710b75b08121b8efd52d786b0d2b6731be"}, + {file = "pyglm-2.8.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a76c8eaba0c58f5738e87be5efcd16c4e75540fe6ebfdf15c236a799a61358e"}, + {file = "pyglm-2.8.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:879ee9ab3c8ab47b1de59fe7e593eda854b8349f274ca60f057b83f3a405b84d"}, + {file = "pyglm-2.8.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:3e37c15b6c3e08f960b34ff9ec42e73469dfd868aec214e8a347da6c9d0245d6"}, + {file = "pyglm-2.8.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd34a8670debef4a55bc756b14cbe8b0a4daa49f8f6850c86c5e11d20554927f"}, + {file = "pyglm-2.8.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:699f852e0335b79d0b664ba1c2d02cb4689256cda786e7780e821f60b0824c46"}, + {file = "pyglm-2.8.3-cp314-cp314t-win_amd64.whl", hash = "sha256:78caadaf9cc2ddea1c55b0d44fa8032f35c9f821a6f152b72422e5657d38f01d"}, + {file = "pyglm-2.8.3-cp314-cp314t-win_arm64.whl", hash = "sha256:69400ad1852ca0972e4d9cbef9d9510941d4b81dc0fffebc5ac796a85440a119"}, + {file = "pyglm-2.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a70c0eeb0db43ae95b3a18f888bb78a6e3807c9eb50080c810a30d9ef6a3bff1"}, + {file = "pyglm-2.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cfd651d124ce62c5de2a547f895979ce4e9c126513e4a5ba39261e9d0b651b15"}, + {file = "pyglm-2.8.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4074ff6e7b4abc4c8f1ea76db6407374524ba311b74d2276c1571ea5ae86b7bc"}, + {file = "pyglm-2.8.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c03082aa1875b5fb68d0491f68491b530d757e9d5b08ac1819605d263b6630c"}, + {file = "pyglm-2.8.3-cp39-cp39-manylinux_2_34_aarch64.whl", hash = "sha256:3408bbd0997cadee48ec9a1422e1396d954a30c23f2e3489327d959bc655fd97"}, + {file = "pyglm-2.8.3-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:3f97b2d5bb02ef0868ec973e2d4dff86bf9b65b6d3f40b4bdc16dc50bc9e1703"}, + {file = "pyglm-2.8.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3205a615aebb89f8584429c4fae490e4b1421f615fc262897a77467c53ecf536"}, + {file = "pyglm-2.8.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1f21818b13d59301d832679d3b81095bf96c2a46f67b8444188acd23c1080561"}, + {file = "pyglm-2.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:f4230df2bbc97424333d66b8550b7ba8865d79d2009dbcb551330cc5221e7430"}, + {file = "pyglm-2.8.3-cp39-cp39-win_arm64.whl", hash = "sha256:8396c7a73d12aa6db735ce5d49f2883fc392c1214376202b0a438c8441bd84b6"}, + {file = "pyglm-2.8.3.tar.gz", hash = "sha256:161781ea4d1267f796b645f85ebff53aeb8ee4f13b4e993c04d64c96d286e534"}, +] + [[package]] name = "pygments" version = "2.20.0" @@ -5752,14 +6430,14 @@ files = [ [[package]] name = "pyjwt" -version = "2.12.1" +version = "2.13.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"}, - {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"}, + {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"}, + {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"}, ] [package.dependencies] @@ -5767,20 +6445,17 @@ cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryp [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] [[package]] name = "pylint" -version = "4.0.5" +version = "4.0.7" description = "python code static checker" optional = false python-versions = ">=3.10.0" groups = ["main"] files = [ - {file = "pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2"}, - {file = "pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c"}, + {file = "pylint-4.0.7-py3-none-any.whl", hash = "sha256:be4a3111557a614411ed1fc89347ce4a8e1013a59e1f33d11485227a02e3304d"}, + {file = "pylint-4.0.7.tar.gz", hash = "sha256:9b2d1d15791c84b77a4fe2aafe8f0d9570717e2dea06d53b19c105cf60275a52"}, ] [package.dependencies] @@ -5798,14 +6473,14 @@ testutils = ["gitpython (>3)"] [[package]] name = "pylint-odoo" -version = "10.0.2" +version = "10.0.9" description = "Pylint plugin for Odoo" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pylint_odoo-10.0.2-py3-none-any.whl", hash = "sha256:d1860b777ef245dae4a9b9884babee334cf2dbcb13f2324de5448b1d925f2814"}, - {file = "pylint_odoo-10.0.2.tar.gz", hash = "sha256:45a1e0d136a73501408068737db7a4966459e79640741a1d71600347a1db8e5c"}, + {file = "pylint_odoo-10.0.9-py3-none-any.whl", hash = "sha256:372f47b4c720c3c9468396b9eb7d851644b2f0e882d24bbd7cc16c27453accbc"}, + {file = "pylint_odoo-10.0.9.tar.gz", hash = "sha256:7ac4e28313bc3a9d5bef016e64daadf33a16de8a2521ca8ecdb9bf65b09845d1"}, ] [package.dependencies] @@ -5829,21 +6504,24 @@ pylint = ">=1.7" [[package]] name = "pymupdf" -version = "1.27.2.3" +version = "1.28.2" description = "A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents." optional = false python-versions = ">=3.10" groups = ["main"] +markers = "platform_machine != \"s390x\"" files = [ - {file = "pymupdf-1.27.2.3-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc1bc3cae6e9e150b0dbb0a9221bdfd411d65f0db2fe359eaa22467d7cc2a05f"}, - {file = "pymupdf-1.27.2.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:660d93cb6da5bbddf11d3982ae27745dd3a9902d9f24cdb69adab83962294b5a"}, - {file = "pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1dd460a3ae4597a755f00a3bd9771f5ebf1531dc111f6a36bf05dd00a6b84425"}, - {file = "pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:857842b4888827bd6155a1131341b2822a7ebe9a8c15a975fd7d490d7a64a30c"}, - {file = "pymupdf-1.27.2.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:580983849c64a08d08344ca3d1580e87c01f046a8392421797bc850efd72a5b6"}, - {file = "pymupdf-1.27.2.3-cp310-abi3-win32.whl", hash = "sha256:a5c1088a87189891a4946ab314a14b7934ac4c5b6077f7e74ebee956f8906d0e"}, - {file = "pymupdf-1.27.2.3-cp310-abi3-win_amd64.whl", hash = "sha256:d20f68ef15195e073071dbc4ae7455257c7889af7584e39df490c0a92728526e"}, - {file = "pymupdf-1.27.2.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:77691604c5d1d0233827139bbcdea61fd57879c84712b8e49b1f45520f7ab9c2"}, - {file = "pymupdf-1.27.2.3.tar.gz", hash = "sha256:7a92faa25129e8bbec5e50eeb9214f187665428c31b05c4ef6e36c58c0b1c6d2"}, + {file = "pymupdf-1.28.2-cp310-abi3-macosx_10_15_x86_64.whl", hash = "sha256:5fc315b425ff1f7afdd1ea2f348205cb19b806767daae7ce4d64115799c2bae1"}, + {file = "pymupdf-1.28.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:7113846b35dbf0a033f088e4f4fb543dabeb4b0b12c112966a1ca1ee2d5eacae"}, + {file = "pymupdf-1.28.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3050a233dde1211efe89ada74e2add6238436434159f46097a1423aad2842545"}, + {file = "pymupdf-1.28.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:397d6715c1f0df7548a92d0afd8ce370fc48fa47aeefac16be2bc04a16a8227f"}, + {file = "pymupdf-1.28.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f89fb2d86d07d643a269f17a093105057e20c79c1d06c103b53600067b6d2b01"}, + {file = "pymupdf-1.28.2-cp310-abi3-win32.whl", hash = "sha256:530ef543a3885b3b81cb72a854e7c5a625a9233201221132bb6c31698c6a2bdb"}, + {file = "pymupdf-1.28.2-cp310-abi3-win_amd64.whl", hash = "sha256:ebd244918798502d7b4504c90410d1711a4d7675a32584ca30f1bab419ecbffe"}, + {file = "pymupdf-1.28.2-cp310-abi3-win_arm64.whl", hash = "sha256:ffe91a24edc75c80da2a4b62f50fc0f54632d34fc8fe4cbc48e5c7ff07cf8fb4"}, + {file = "pymupdf-1.28.2-cp313-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2e1b574c0fd2cb238021033fd3c0f9c4388816638df064e4bfb56d9d81736dc8"}, + {file = "pymupdf-1.28.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:fd481ed48bef56305c41fb7e05a055c03345c899c7b101dad086258b438f8168"}, + {file = "pymupdf-1.28.2.tar.gz", hash = "sha256:5e0be7908a715aa20333caddd73f1d6f01e4cd0c26e869fa2dd0b7f344da2249"}, ] [[package]] @@ -5903,6 +6581,18 @@ files = [ requests = ">=2.0.1" six = "*" +[[package]] +name = "pyopengl" +version = "3.1.10" +description = "Standard OpenGL bindings for Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pyopengl-3.1.10-py3-none-any.whl", hash = "sha256:794a943daced39300879e4e47bd94525280685f42dbb5a998d336cfff151d74f"}, + {file = "pyopengl-3.1.10.tar.gz", hash = "sha256:c4a02d6866b54eb119c8e9b3fb04fa835a95ab802dd96607ab4cdb0012df8335"}, +] + [[package]] name = "pyopenssl" version = "26.0.0" @@ -5940,23 +6630,25 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pypdf" -version = "6.10.2" +version = "6.15.0" description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pypdf-6.10.2-py3-none-any.whl", hash = "sha256:aa53be9826655b51c96741e5d7983ca224d898ac0a77896e64636810517624aa"}, - {file = "pypdf-6.10.2.tar.gz", hash = "sha256:7d09ce108eff6bf67465d461b6ef352dcb8d84f7a91befc02f904455c6eea11d"}, + {file = "pypdf-6.15.0-py3-none-any.whl", hash = "sha256:14e001d6504822cb1ca9c7ed9a69bccb320f59b320730f55af804361abe4d5ee"}, + {file = "pypdf-6.15.0.tar.gz", hash = "sha256:d39c4d955a76409284a905e2d65b40076d77ab76129e0faaeeb6612403ecfc79"}, ] [package.extras] -crypto = ["cryptography"] +crypto = ["cryptography (>3.0)"] cryptodome = ["PyCryptodome"] dev = ["flit", "pip-tools", "pre-commit", "pytest-cov", "pytest-socket", "pytest-timeout", "pytest-xdist", "wheel"] docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"] -full = ["Pillow (>=8.0.0)", "cryptography"] +fonts = ["fonttools"] +full = ["Pillow (>=8.0.0)", "arabic-reshaper", "cryptography (>3.0)", "fonttools", "python-bidi"] image = ["Pillow (>=8.0.0)"] +rtl-text = ["arabic-reshaper", "python-bidi"] [[package]] name = "pypdf2" @@ -6070,20 +6762,20 @@ files = [ [[package]] name = "pyrdfa3" -version = "3.6.5" +version = "3.6.2" description = "pyRdfa distiller/parser library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "pyrdfa3-3.6.5-py3-none-any.whl", hash = "sha256:3c0d22e2949e7b3abd004fff7c7f110faa83a18bec06f590bdab2ef0f1ee3c02"}, - {file = "pyrdfa3-3.6.5.tar.gz", hash = "sha256:0fc28ff1426af805b100aff7162db6a43fa215e37f92bce9b113b465feb563ea"}, + {file = "pyRdfa3-3.6.2-py3-none-any.whl", hash = "sha256:290c2fa966ddd1b45ac94a727da144f5a233ed58c63c370e3d68e6d00b0dee5d"}, + {file = "pyRdfa3-3.6.2.tar.gz", hash = "sha256:73681dab957f60901696767388b956a5769c730bc451da6ffb2f0e36f18314c2"}, ] [package.dependencies] html5lib = ">=1.1" -rdflib = ">=7.0.0" -requests = ">=2.32.3" +rdflib = ">=6.1.1" +requests = ">=2.25.1" [[package]] name = "pysaml2" @@ -6276,14 +6968,14 @@ pyasn1_modules = ">=0.1.5" [[package]] name = "python-multipart" -version = "0.0.27" +version = "0.0.32" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645"}, - {file = "python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602"}, + {file = "python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23"}, + {file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"}, ] [[package]] @@ -6357,14 +7049,14 @@ soap-fallback = ["PySimpleSOAP"] [[package]] name = "python-telegram-bot" -version = "22.7" +version = "22.8" description = "We have made you a wrapper you can't refuse" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "python_telegram_bot-22.7-py3-none-any.whl", hash = "sha256:d72eed532cf763758cd9331b57a6d790aff0bb4d37d8f4e92149436fe21c6475"}, - {file = "python_telegram_bot-22.7.tar.gz", hash = "sha256:784b59ea3852fe4616ad63b4a0264c755637f5d725e87755ecdee28300febf61"}, + {file = "python_telegram_bot-22.8-py3-none-any.whl", hash = "sha256:42373918097f1b837cc4e717d588c19ea79651497ec712bb5b0c76e5e63c50e1"}, + {file = "python_telegram_bot-22.8.tar.gz", hash = "sha256:f9d3847fcb23ee603477e442800b33bb4adf851a73e0619d2050be879decf1ef"}, ] [package.dependencies] @@ -6401,16 +7093,45 @@ docs = ["mock", "python-utils", "sphinx"] loguru = ["loguru"] tests = ["blessings", "loguru", "loguru-mypy", "mypy-ipython", "pyright", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mypy", "ruff", "sphinx", "types-setuptools"] +[[package]] +name = "pythonopenscad" +version = "2.2.19" +description = "Yet Another Python OpenSCAD API. This is a thin wrapper for generating OpenSCAD 3D model scripts." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pythonopenscad-2.2.19-py3-none-any.whl", hash = "sha256:387403c9f3d7873cd6e913175ed53622b3aef4ca38d8cc16aaaa3cad76d6cb40"}, + {file = "pythonopenscad-2.2.19.tar.gz", hash = "sha256:0d41da25b53b1efa7f9895003aa87a71855a05268bb678262238144799e9c5cc"}, +] + +[package.dependencies] +anchorscad-linear = ">=0.1.1" +datatrees = ">=0.2.1" +distributed = ">=2025.1.0" +fonttools = ">=4.57.0" +frozendict = ">=2.3.0" +hershey-fonts = ">=2.1.0" +manifold3d = ">=3.1.0" +mapbox = ">=0.18.1" +mapbox-earcut = ">=1.0.3" +numpy = ">=2.2.1" +numpy-stl = ">=3.2.0" +pillow = ">=11.0.0" +pyglm = ">=2.8.0" +pyopengl = ">=3.1.9" +uharfbuzz = ">=0.48.0" + [[package]] name = "pytz" -version = "2026.2" +version = "2026.3.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126"}, - {file = "pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a"}, + {file = "pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815"}, + {file = "pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d"}, ] [[package]] @@ -6666,20 +7387,20 @@ files = [ [[package]] name = "redis" -version = "7.4.0" +version = "8.1.0" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec"}, - {file = "redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad"}, + {file = "redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb"}, + {file = "redis-8.1.0.tar.gz", hash = "sha256:6e1a19beef9225c83efd689c7e6b7da2d5215b1f42cd13b7fc3714d0a09c7b25"}, ] [package.extras] circuit-breaker = ["pybreaker (>=1.4.0)"] hiredis = ["hiredis (>=3.2.0)"] -jwt = ["pyjwt (>=2.9.0)"] +jwt = ["pyjwt (>=2.13.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] otel = ["opentelemetry-api (>=1.39.1)", "opentelemetry-exporter-otlp-proto-http (>=1.39.1)", "opentelemetry-sdk (>=1.39.1)"] xxhash = ["xxhash (>=3.6.0,<3.7.0)"] @@ -6703,126 +7424,126 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "regex" -version = "2026.4.4" +version = "2026.7.19" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "regex-2026.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:74fa82dcc8143386c7c0392e18032009d1db715c25f4ba22d23dc2e04d02a20f"}, - {file = "regex-2026.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a85b620a388d6c9caa12189233109e236b3da3deffe4ff11b84ae84e218a274f"}, - {file = "regex-2026.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2895506ebe32cc63eeed8f80e6eae453171cfccccab35b70dc3129abec35a5b8"}, - {file = "regex-2026.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6780f008ee81381c737634e75c24e5a6569cc883c4f8e37a37917ee79efcafd9"}, - {file = "regex-2026.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88e9b048345c613f253bea4645b2fe7e579782b82cac99b1daad81e29cc2ed8e"}, - {file = "regex-2026.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be061028481186ba62a0f4c5f1cc1e3d5ab8bce70c89236ebe01023883bc903b"}, - {file = "regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2228c02b368d69b724c36e96d3d1da721561fb9cc7faa373d7bf65e07d75cb5"}, - {file = "regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0540e5b733618a2f84e9cb3e812c8afa82e151ca8e19cf6c4e95c5a65198236f"}, - {file = "regex-2026.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf9b1b2e692d4877880388934ac746c99552ce6bf40792a767fd42c8c99f136d"}, - {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:011bb48bffc1b46553ac704c975b3348717f4e4aa7a67522b51906f99da1820c"}, - {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8512fcdb43f1bf18582698a478b5ab73f9c1667a5b7548761329ef410cd0a760"}, - {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:867bddc63109a0276f5a31999e4c8e0eb7bbbad7d6166e28d969a2c1afeb97f9"}, - {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1b9a00b83f3a40e09859c78920571dcb83293c8004079653dd22ec14bbfa98c7"}, - {file = "regex-2026.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e355be718caf838aa089870259cf1776dc2a4aa980514af9d02c59544d9a8b22"}, - {file = "regex-2026.4.4-cp310-cp310-win32.whl", hash = "sha256:33bfda9684646d323414df7abe5692c61d297dbb0530b28ec66442e768813c59"}, - {file = "regex-2026.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:0709f22a56798457ae317bcce42aacee33c680068a8f14097430d9f9ba364bee"}, - {file = "regex-2026.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:ee9627de8587c1a22201cb16d0296ab92b4df5cdcb5349f4e9744d61db7c7c98"}, - {file = "regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6"}, - {file = "regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87"}, - {file = "regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8"}, - {file = "regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada"}, - {file = "regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d"}, - {file = "regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87"}, - {file = "regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4"}, - {file = "regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86"}, - {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59"}, - {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453"}, - {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80"}, - {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b"}, - {file = "regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f"}, - {file = "regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351"}, - {file = "regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735"}, - {file = "regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54"}, - {file = "regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52"}, - {file = "regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb"}, - {file = "regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76"}, - {file = "regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be"}, - {file = "regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1"}, - {file = "regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13"}, - {file = "regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9"}, - {file = "regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d"}, - {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3"}, - {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0"}, - {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043"}, - {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244"}, - {file = "regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73"}, - {file = "regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f"}, - {file = "regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b"}, - {file = "regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983"}, - {file = "regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943"}, - {file = "regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031"}, - {file = "regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7"}, - {file = "regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17"}, - {file = "regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17"}, - {file = "regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae"}, - {file = "regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e"}, - {file = "regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d"}, - {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27"}, - {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf"}, - {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0"}, - {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa"}, - {file = "regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b"}, - {file = "regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62"}, - {file = "regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81"}, - {file = "regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427"}, - {file = "regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c"}, - {file = "regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141"}, - {file = "regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717"}, - {file = "regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07"}, - {file = "regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca"}, - {file = "regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520"}, - {file = "regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883"}, - {file = "regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b"}, - {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1"}, - {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b"}, - {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff"}, - {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb"}, - {file = "regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4"}, - {file = "regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa"}, - {file = "regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0"}, - {file = "regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe"}, - {file = "regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7"}, - {file = "regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752"}, - {file = "regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951"}, - {file = "regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f"}, - {file = "regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8"}, - {file = "regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4"}, - {file = "regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9"}, - {file = "regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83"}, - {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb"}, - {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465"}, - {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4"}, - {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566"}, - {file = "regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95"}, - {file = "regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8"}, - {file = "regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4"}, - {file = "regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f"}, - {file = "regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3"}, - {file = "regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e"}, - {file = "regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6"}, - {file = "regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359"}, - {file = "regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a"}, - {file = "regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55"}, - {file = "regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99"}, - {file = "regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790"}, - {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc"}, - {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f"}, - {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863"}, - {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a"}, - {file = "regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81"}, - {file = "regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74"}, - {file = "regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45"}, - {file = "regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d"}, - {file = "regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423"}, + {file = "regex-2026.7.19-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:555497390743af1a65045fa4527782d10ff5b88970359412baa4a1e628fe393b"}, + {file = "regex-2026.7.19-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:343a4504e3fb688c47cad451221ca5d4814f42b1e16c0065bde9cbf7f473bd52"}, + {file = "regex-2026.7.19-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ebee1ee89c39c953baac6924fcde08c5bb427c4057510862f9d7c7bdb3d8665"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:062f8cb7a9739c4835d22bd96f370c59aba89f257adcfa53be3cc209e08d3ae0"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1123ef4211d763ee771d47916a1596e2f4915794f7aabdc1adcb20e4249a6951"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6e44c0e7c5664be20aee92085153150c0a7967310a73a43c0f832b7cd35d0dd3"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98c6ac18480fcdb33f35439183f1d2e79760ab41930309c6d951cb1f8e46694c"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4458124d71339f505bf1fb94f69fd1bb8fa9d2481eebfef27c10ef4f2b9e12f6"}, + {file = "regex-2026.7.19-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbf300e2070bb35038660b3be1be4b91b0024edb41517e6996320b49b92b4175"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b2b506b1788df5fecd270a10d5e70a95fe77b87ea2b370a318043f6f5f817ee6"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:52579c60a6078be70a0e49c81d6e56d677f34cd439af281a0083b8c7bc75c095"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:2955907b7157a6660f27079edf7e0229e9c9c5325c77a2ef6a890cba91efa6f0"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:89dfee3319f5ae3f75ebd5c2445a809bb320252ba5529ffdafea4ef25d79cf1a"}, + {file = "regex-2026.7.19-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d3143f159261b1ce5b24c261c590e5913370c3200c5e9ebbb92b5aa5e111902"}, + {file = "regex-2026.7.19-cp310-cp310-win32.whl", hash = "sha256:64729333167c2dcaaa56a331d40ee097bd9c5617ffd51dabb09eaddafb1b532e"}, + {file = "regex-2026.7.19-cp310-cp310-win_amd64.whl", hash = "sha256:1c398716054621aa300b3d411f467dda903806c5da0df6945ab73982b8d115db"}, + {file = "regex-2026.7.19-cp310-cp310-win_arm64.whl", hash = "sha256:064f1760a5a4ade65c5419be23e782f29147528e8a66e0c42dd4cedb8d4e9fc6"}, + {file = "regex-2026.7.19-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ac777001cdfc28b72477d93c8564bb7583081ea8fb45cdca3d568e0a4f87183c"}, + {file = "regex-2026.7.19-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:59787bd5f8c70aa339084e961d2996b53fbdeab4d5393bba5c1fe1fc32e02bae"}, + {file = "regex-2026.7.19-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:90c633e7e8d6bf4e992b8b36ce69e018f834b641dd6de8cea6d78c06ffa119c5"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ccab0db8d5f4fbb0272642113c1adb2ffc698c16d3a0944580222331fa7a20"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e50d748a32da622f256e8d505867f5d3c43a837c6a9f0efb149655fadd1042a"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf1516fe58fc104f39b2d1dbe2d5e27d0cd45c4be2e42ba6ee0cc763701ec3c7"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09f3e5287f94f17b709dc9a9e70865855feee835c861613be144218ce4ca82cc"}, + {file = "regex-2026.7.19-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6383cd2ed53a646c659ba1fe65727db76437fdaa069e697a0b44a51d5843d864"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:09d3007fc76249a83cdd33de160d50e6cb77f54e09d8fa9e7148e10607ce24af"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f8c6e7a1cfa3dc9d0ee2de0e65e834537fa29992cc3976ffec914afc35c5dd5"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b2ea4a3e8357be8849e833beeae757ac3c7a6b3fc055c03c808a53c91ad30d82"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:80115dd39481fd3a4b4080220799dbcacb921a844de4b827264ececacbe17c78"}, + {file = "regex-2026.7.19-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6ce43a0269d68cee79a7d1ade7def53c20f8f2a047b92d7b5d5bcc73ae88327"}, + {file = "regex-2026.7.19-cp311-cp311-win32.whl", hash = "sha256:9be2a6647740dd3cca6acb24e87f03d7632cd280dbce9bbe40c26353a215a45d"}, + {file = "regex-2026.7.19-cp311-cp311-win_amd64.whl", hash = "sha256:8d3469c91dd92ee41b7c95280edbd975ef1ba9195086686623a1c6e8935ce965"}, + {file = "regex-2026.7.19-cp311-cp311-win_arm64.whl", hash = "sha256:36aacfb15faaff3ced55afbf35ec72f50d4aee22082c4f7fe0573a33e2fca92e"}, + {file = "regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d"}, + {file = "regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd"}, + {file = "regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68"}, + {file = "regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035"}, + {file = "regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a"}, + {file = "regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5"}, + {file = "regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312"}, + {file = "regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d"}, + {file = "regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40"}, + {file = "regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38"}, + {file = "regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15"}, + {file = "regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc"}, + {file = "regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2"}, + {file = "regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404"}, + {file = "regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e"}, + {file = "regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8"}, + {file = "regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2"}, + {file = "regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda"}, + {file = "regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a"}, + {file = "regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e"}, + {file = "regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0"}, + {file = "regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4"}, + {file = "regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974"}, + {file = "regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d"}, + {file = "regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd"}, + {file = "regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac"}, + {file = "regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a"}, + {file = "regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97"}, + {file = "regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4"}, + {file = "regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa"}, + {file = "regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac"}, + {file = "regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459"}, + {file = "regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3"}, + {file = "regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518"}, + {file = "regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276"}, + {file = "regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966"}, + {file = "regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44"}, + {file = "regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78"}, + {file = "regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2"}, + {file = "regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547"}, + {file = "regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5"}, ] [[package]] @@ -6848,25 +7569,25 @@ renderpm = ["rl-renderPM (>=4.0.3,<4.1)"] [[package]] name = "requests" -version = "2.33.1" +version = "2.31.0" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.10" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, - {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] -certifi = ">=2023.5.7" -charset_normalizer = ">=2,<4" +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.26,<3" +urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-file" @@ -6919,14 +7640,14 @@ requests = ">=2.0.1,<3.0.0" [[package]] name = "responses" -version = "0.26.0" +version = "0.26.2" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "responses-0.26.0-py3-none-any.whl", hash = "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37"}, - {file = "responses-0.26.0.tar.gz", hash = "sha256:c7f6923e6343ef3682816ba421c006626777893cb0d5e1434f674b649bac9eb4"}, + {file = "responses-0.26.2-py3-none-any.whl", hash = "sha256:6fdfeabd58e5ec473b98dfe02e6d46d3173bd8dd573eff2ccccf1a05a5135364"}, + {file = "responses-0.26.2.tar.gz", hash = "sha256:9c9259b46a8349197edebf43cfa68a87e1a2802ef503ff8b2fecbabc0b45afd8"}, ] [package.dependencies] @@ -7169,20 +7890,20 @@ msg-parse = ["extract_msg (>=0.27,<1.0)"] [[package]] name = "s3fs" -version = "2026.4.0" +version = "2026.7.0" description = "Convenient Filesystem interface over S3" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "s3fs-2026.4.0-py3-none-any.whl", hash = "sha256:de0d2a1f33cdf03831fd2382d278c6e4e31fe57c3bf2f703c61f8aec6b703e2a"}, - {file = "s3fs-2026.4.0.tar.gz", hash = "sha256:5bdce0abb00b0435ee150807a45fea727451dbc22de4cbc116464f8504ab9d37"}, + {file = "s3fs-2026.7.0-py3-none-any.whl", hash = "sha256:64edf3c01ebffab1eec38ff9c09eefbf86a3db14c87d248f795da0e7b801d698"}, + {file = "s3fs-2026.7.0.tar.gz", hash = "sha256:76b062d1b2bc7bf4bcd9e7d8f1eb2b5dd9d5cee96ce888664c4ddb5f563146bf"}, ] [package.dependencies] aiobotocore = ">=2.19.0,<4.0.0" aiohttp = ">=3.9.0,<4.0.0a0 || >4.0.0a0,<4.0.0a1 || >4.0.0a1" -fsspec = "2026.4.0" +fsspec = ">=2026.7.0,<2026.7.1" [[package]] name = "s3transfer" @@ -7202,46 +7923,6 @@ botocore = ">=1.37.4,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] -[[package]] -name = "saxonche" -version = "12.9.0" -description = "Official Saxonica python package for the SaxonC-HE 12.9.0 processor: for XSLT 3.0, XQuery 3.1, XPath 3.1 and XML Schema processing." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "saxonche-12.9.0-cp310-cp310-macosx_10_11_x86_64.whl", hash = "sha256:4a76290c863c1057d291bdc1ca7041e191f4fa76723ee58b34f5d647508830b6"}, - {file = "saxonche-12.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:de8f66af504540b65909ca43641c4c9adf3c143ed71c9101b897a05d806369a9"}, - {file = "saxonche-12.9.0-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:f274f3fbd016a78b257b8c1a85724a64bef638c7c03fe9a84c95c11f720b9f95"}, - {file = "saxonche-12.9.0-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:2136e04b4d53f21baef10014c3648748242059d7a88215fe500b0e54bd9ce30c"}, - {file = "saxonche-12.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:50cd46ccf63473e601f6685d2be00347c9fee6400e361069957674f2f2361607"}, - {file = "saxonche-12.9.0-cp311-cp311-macosx_10_11_x86_64.whl", hash = "sha256:0beb30451441178dd0115923429ff4334d29869afefcf703a1eb23204bae4bca"}, - {file = "saxonche-12.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e51ccb9145d06d9187441a893c3e899f4877115c7b95a6a282338ade939a6b8"}, - {file = "saxonche-12.9.0-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:6563769b6e3643cbbf719f2b1f38a9c7461d7d8dde9501a77c5b856c27a618e2"}, - {file = "saxonche-12.9.0-cp311-cp311-manylinux_2_24_x86_64.whl", hash = "sha256:865ab2c80d93d7048166629d134289e1363ec14777003b93779a45798e42e0c6"}, - {file = "saxonche-12.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:dcd52acb2476b0155949e9734197792f32678d589621cc6ecde8eed2153c3306"}, - {file = "saxonche-12.9.0-cp312-cp312-macosx_10_11_x86_64.whl", hash = "sha256:c039743bb4ae420aa4cdaafae88048a9735d3fa8e7b3d892d28ae49528dba3b1"}, - {file = "saxonche-12.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9972af6b96a3747e4b6fa7a76e036d2bc483108dc71feb9f780ece51e34e06be"}, - {file = "saxonche-12.9.0-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:52cd769cef9b689ef7d73d497ecfcbaf79be0ae1f51ad7190cc2ba6b0b05c3f1"}, - {file = "saxonche-12.9.0-cp312-cp312-manylinux_2_24_x86_64.whl", hash = "sha256:490f30e9486750f6a066de2b467114dfd6e14d23c8ce645cad64e663f580490a"}, - {file = "saxonche-12.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:bfa240542d8e63f6c9b251c120aaea76cfdf3055aad8cfed05cf60af120333c6"}, - {file = "saxonche-12.9.0-cp313-cp313-macosx_10_11_x86_64.whl", hash = "sha256:a67015038d4abb039a51b935af2e986a83138f9fe26c83f288e9727644bf83d7"}, - {file = "saxonche-12.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:baac3f9c0aceb6271d03a87005f6c531aabac23b1eae3eecb1dc65d7f8817e91"}, - {file = "saxonche-12.9.0-cp313-cp313-manylinux_2_24_aarch64.whl", hash = "sha256:5b16a9e5ae78be24f83da6a2c5e698b59ca4de8f6880022f051351170b69f9d0"}, - {file = "saxonche-12.9.0-cp313-cp313-manylinux_2_24_x86_64.whl", hash = "sha256:b7d295ddeae3e7c355cf53035ec47a1db301a8b9bc917636f893b56a31a48187"}, - {file = "saxonche-12.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:744fa6e4e0f76797f7c904ac639a25ba5ec36f59f20cbab8e0f2a04cacf97b57"}, - {file = "saxonche-12.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3b45146770f6459e10c70911fd28e8741fcd2cde07b53639f2008f98d5b15067"}, - {file = "saxonche-12.9.0-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:cb5934037737fb7972f53e9408165f187c4ecea5ef213fcdbc21b786f467a784"}, - {file = "saxonche-12.9.0-cp314-cp314-manylinux_2_24_aarch64.whl", hash = "sha256:a1a79b6e713aeb94cc30815253bde96c6dfabe8ea6f214969ed0e4ae60980f68"}, - {file = "saxonche-12.9.0-cp314-cp314-manylinux_2_24_x86_64.whl", hash = "sha256:b6ad3f907a87111772b887b117542b4fd030253c236b1bb71902972cefcf55da"}, - {file = "saxonche-12.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:5961f5ff8f83eebc635c5b6730e2ed0e0346437540f1cb39062b08dc273b3f94"}, - {file = "saxonche-12.9.0-cp39-cp39-macosx_10_11_x86_64.whl", hash = "sha256:1612aaf20fcd79db40861568c7126a220de23eb7965c941733a0800ce426aadb"}, - {file = "saxonche-12.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7334c71b49cc0aa2473eb17bccd461f818113736cbe6f2497f28a35b0aa3b14"}, - {file = "saxonche-12.9.0-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:f9fa5666a32238e509a4ba440d85c5f1a20786af479fcc255ea61ce426fd3477"}, - {file = "saxonche-12.9.0-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:dfb1bf5317189cd40a6dac4968df03e13b80d946c324afc4800971e06ff754fd"}, - {file = "saxonche-12.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:00276175ce009623e12c0933f983d4b9eb34d657e4af96e63c407ca44595cd97"}, -] - [[package]] name = "schwifty" version = "2024.4.0" @@ -7449,6 +8130,33 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sortedcollections" +version = "2.1.0" +description = "Python Sorted Collections" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "sortedcollections-2.1.0-py3-none-any.whl", hash = "sha256:b07abbc73472cc459da9dd6e2607d73d1f3b9309a32dd9a57fa2c6fa882f4c6c"}, + {file = "sortedcollections-2.1.0.tar.gz", hash = "sha256:d8e9609d6c580a16a1224a3dc8965789e03ebc4c3e5ffd05ada54a2fed5dcacd"}, +] + +[package.dependencies] +sortedcontainers = "*" + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "soupsieve" version = "2.8.3" @@ -7463,75 +8171,70 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.49" +version = "2.0.51" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f"}, - {file = "sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b"}, - {file = "sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1"}, - {file = "sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339"}, - {file = "sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d"}, - {file = "sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3"}, - {file = "sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75"}, - {file = "sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe"}, - {file = "sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014"}, - {file = "sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536"}, - {file = "sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88"}, - {file = "sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700"}, - {file = "sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a"}, - {file = "sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af"}, - {file = "sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b"}, - {file = "sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982"}, - {file = "sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672"}, - {file = "sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e"}, - {file = "sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750"}, - {file = "sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0"}, - {file = "sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4"}, - {file = "sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120"}, - {file = "sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2"}, - {file = "sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3"}, - {file = "sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7"}, - {file = "sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33"}, - {file = "sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b"}, - {file = "sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148"}, - {file = "sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518"}, - {file = "sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d"}, - {file = "sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0"}, - {file = "sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08"}, - {file = "sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d"}, - {file = "sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba"}, - {file = "sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e"}, - {file = "sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a"}, - {file = "sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066"}, - {file = "sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187"}, - {file = "sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401"}, - {file = "sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5"}, - {file = "sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5"}, - {file = "sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977"}, - {file = "sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01"}, - {file = "sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61"}, - {file = "sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a"}, - {file = "sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158"}, - {file = "sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7"}, - {file = "sqlalchemy-2.0.49-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8a97ac839c2c6672c4865e48f3cbad7152cee85f4233fb4ca6291d775b9b954a"}, - {file = "sqlalchemy-2.0.49-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c338ec6ec01c0bc8e735c58b9f5d51e75bacb6ff23296658826d7cfdfdb8678a"}, - {file = "sqlalchemy-2.0.49-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:566df36fd0e901625523a5a1835032f1ebdd7f7886c54584143fa6c668b4df3b"}, - {file = "sqlalchemy-2.0.49-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d99945830a6f3e9638d89a28ed130b1eb24c91255e4f24366fbe699b983f29e4"}, - {file = "sqlalchemy-2.0.49-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:01146546d84185f12721a1d2ce0c6673451a7894d1460b592d378ca4871a0c72"}, - {file = "sqlalchemy-2.0.49-cp38-cp38-win32.whl", hash = "sha256:69469ce8ce7a8df4d37620e3163b71238719e1e2e5048d114a1b6ce0fbf8c662"}, - {file = "sqlalchemy-2.0.49-cp38-cp38-win_amd64.whl", hash = "sha256:b95b2f470c1b2683febd2e7eab1d3f0e078c91dbdd0b00e9c645d07a413bb99f"}, - {file = "sqlalchemy-2.0.49-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43d044780732d9e0381ac8d5316f95d7f02ef04d6e4ef6dc82379f09795d993f"}, - {file = "sqlalchemy-2.0.49-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d6be30b2a75362325176c036d7fb8d19e8846c77e87683ffaa8177b35135613"}, - {file = "sqlalchemy-2.0.49-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d898cc2c76c135ef65517f4ddd7a3512fb41f23087b0650efb3418b8389a3cd1"}, - {file = "sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:059d7151fff513c53a4638da8778be7fce81a0c4854c7348ebd0c4078ddf28fe"}, - {file = "sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:334edbcff10514ad1d66e3a70b339c0a29886394892490119dbb669627b17717"}, - {file = "sqlalchemy-2.0.49-cp39-cp39-win32.whl", hash = "sha256:74ab4ee7794d7ed1b0c37e7333640e0f0a626fc7b398c07a7aef52f484fddde3"}, - {file = "sqlalchemy-2.0.49-cp39-cp39-win_amd64.whl", hash = "sha256:88690f4e1f0fbf5339bedbb127e240fec1fd3070e9934c0b7bef83432f779d2f"}, - {file = "sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0"}, - {file = "sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-win32.whl", hash = "sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-win_amd64.whl", hash = "sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-win32.whl", hash = "sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-win_amd64.whl", hash = "sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7"}, + {file = "sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5"}, + {file = "sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9"}, ] [package.dependencies] @@ -7596,32 +8299,43 @@ files = [ [[package]] name = "stevedore" -version = "5.7.0" +version = "5.9.0" description = "Manage dynamic plugins for Python applications" optional = false -python-versions = ">=3.10" +python-versions = ">=3.11" groups = ["main"] files = [ - {file = "stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed"}, - {file = "stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3"}, + {file = "stevedore-5.9.0-py3-none-any.whl", hash = "sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7"}, + {file = "stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c"}, ] [[package]] name = "stone" -version = "3.3.1" +version = "3.5.3" description = "Stone is an interface description language (IDL) for APIs." optional = false -python-versions = "*" +python-versions = ">=3.11" groups = ["main"] files = [ - {file = "stone-3.3.1-py2-none-any.whl", hash = "sha256:cd2f7f9056fc39b16c8fd46a26971dc5ccd30b5c2c246566cd2c0dd27ff96609"}, - {file = "stone-3.3.1-py3-none-any.whl", hash = "sha256:e15866fad249c11a963cce3bdbed37758f2e88c8ff4898616bc0caeb1e216047"}, - {file = "stone-3.3.1.tar.gz", hash = "sha256:4ef0397512f609757975f7ec09b35639d72ba7e3e17ce4ddf399578346b4cb50"}, + {file = "stone-3.5.3-py3-none-any.whl", hash = "sha256:5d78bd0b60692756b44758d876d74e4ed2ed938e29aa62646e3cbc247844027b"}, + {file = "stone-3.5.3.tar.gz", hash = "sha256:d0d99f14d154452e71b3e2752efdbbb1769e4fecbc1291b1dfd1a046f7b6a1cb"}, ] [package.dependencies] -ply = ">=3.4" -six = ">=1.12.0" +Jinja2 = ">=3.0.3" +packaging = ">=21.0" + +[[package]] +name = "tblib" +version = "3.2.2" +description = "Traceback serialization library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "tblib-3.2.2-py3-none-any.whl", hash = "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76"}, + {file = "tblib-3.2.2.tar.gz", hash = "sha256:e9a652692d91bf4f743d4a15bc174c0b76afc750fe8c7b6d195cc1c1d6d2ccec"}, +] [[package]] name = "text-unidecode" @@ -7676,14 +8390,14 @@ test = ["coverage", "fake.py", "pytest", "pytest-codeblock", "pytest-cov", "pyte [[package]] name = "tldextract" -version = "5.3.1" +version = "5.3.2" description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "tldextract-5.3.1-py3-none-any.whl", hash = "sha256:6bfe36d518de569c572062b788e16a659ccaceffc486d243af0484e8ecf432d9"}, - {file = "tldextract-5.3.1.tar.gz", hash = "sha256:a72756ca170b2510315076383ea2993478f7da6f897eef1f4a5400735d5057fb"}, + {file = "tldextract-5.3.2-py3-none-any.whl", hash = "sha256:6c90d2a259f5c89f4fcf01f97af15708416a59ffedddff006d67222ab30d0fb0"}, + {file = "tldextract-5.3.2.tar.gz", hash = "sha256:c017431bc0800f2d3d1b57cce36e06668f0930f60a6d8c4615d4e2b8da298fa9"}, ] [package.dependencies] @@ -7692,20 +8406,51 @@ idna = "*" requests = ">=2.1.0" requests-file = ">=1.4" -[package.extras] -release = ["build", "twine"] -testing = ["mypy", "pytest", "pytest-gitignore", "pytest-mock", "responses", "ruff", "syrupy", "tox", "tox-uv", "types-filelock", "types-requests"] +[[package]] +name = "to-3mf" +version = "0.1.0" +description = "A set of Python tools for managing the 3mf file format. Includes an STL to 3mf file converter." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "to_3mf-0.1.0-py3-none-any.whl", hash = "sha256:00104128fe232cec794fea98e9a3807cceeff10949da72ee3e0c0143e204801e"}, + {file = "to_3mf-0.1.0.tar.gz", hash = "sha256:a5c0fc8f7eb72994ff4e5f59964a54c2046ed7856848d64ca54bb97219b21992"}, +] + +[package.dependencies] +anchorscad-linear = "*" +anchorscad-test-tools = "*" +anchorscad-utils = "*" +datatrees = "*" +lxml = "*" +numpy = "*" +numpy-stl = "*" +pythonopenscad = "*" +xdatatrees = ">=0.1.0" [[package]] name = "tomlkit" -version = "0.14.0" +version = "0.15.1" description = "Style preserving TOML library" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, - {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, + {file = "tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304"}, + {file = "tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97"}, +] + +[[package]] +name = "toolz" +version = "1.1.0" +description = "List processing tools and functional utilities" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8"}, + {file = "toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b"}, ] [[package]] @@ -7752,14 +8497,14 @@ telegram = ["requests"] [[package]] name = "tweepy" -version = "4.16.0" +version = "4.17.0" description = "Library for accessing the X API (Twitter)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "tweepy-4.16.0-py3-none-any.whl", hash = "sha256:48d1a1eb311d2c4b8990abcfa6f9fa2b2ad61be05c723b1a9b4f242656badae2"}, - {file = "tweepy-4.16.0.tar.gz", hash = "sha256:1d95cbdc50bf6353a387f881f2584eaf60d14e00dbbdd8872a73de79c66878e3"}, + {file = "tweepy-4.17.0-py3-none-any.whl", hash = "sha256:1244d7f2959091542f8e4a2c0c4256294e3beb69c0aa26eaf634b126d81eee49"}, + {file = "tweepy-4.17.0.tar.gz", hash = "sha256:c232e9fffa0b15b81d5ff8ec59d46af85e330285ffc3e64614d2418fb9af71b5"}, ] [package.dependencies] @@ -7774,14 +8519,14 @@ test = ["urllib3 (<2)", "vcrpy (>=1.10.3)"] [[package]] name = "twilio" -version = "9.10.5" +version = "9.10.9" description = "Twilio API client and TwiML generator" optional = false python-versions = ">=3.7.0" groups = ["main"] files = [ - {file = "twilio-9.10.5-py2.py3-none-any.whl", hash = "sha256:7972db54496fbf501b238f34d1f717f80ff22720313dc706632787aad5934997"}, - {file = "twilio-9.10.5.tar.gz", hash = "sha256:d9f93b9280349ee7b52e7f17a0600fd7bfd0f7ff88eb00c40270164bc058743f"}, + {file = "twilio-9.10.9-py2.py3-none-any.whl", hash = "sha256:1c50bfb394b5dbc044bacab24b2e3b550bee0c08da51c4a1fa4816293303e66c"}, + {file = "twilio-9.10.9.tar.gz", hash = "sha256:eb74fc026c85a89372836414f57e262119efaa160b9419cf4d05b59056b8e89d"}, ] [package.dependencies] @@ -7792,14 +8537,14 @@ requests = ">=2.0.0" [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] [[package]] @@ -7847,90 +8592,132 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] +[[package]] +name = "uharfbuzz" +version = "0.56.0" +description = "Streamlined Cython bindings for the harfbuzz shaping engine" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "uharfbuzz-0.56.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:86ceb18ce68cef062224ed9919f4925624f4d8e33869e27fe1d8edef1eba83e1"}, + {file = "uharfbuzz-0.56.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:beec7ae03ec5d0d130ca317795ff2ba24f75226a031c26a921e1706f8c59f36c"}, + {file = "uharfbuzz-0.56.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b4bb4398270bbb8e1005a7a3d56acd5258c5b00484a06633cb30776da9cc3ab"}, + {file = "uharfbuzz-0.56.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:032382a82e3b9e2a460116aee2934bb713e4757efb68d7f11f55dbbb1ae57184"}, + {file = "uharfbuzz-0.56.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3a80b0d94baf19be47708903246e2ff4bd1069e1f6bf9ed530bf1c9e3e476b4"}, + {file = "uharfbuzz-0.56.0-cp310-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:f3631cb67335d94318d7b9514f8d5d860a68780d523a188317ecf7861754a395"}, + {file = "uharfbuzz-0.56.0-cp310-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:2ac2c4d59d0059265cad9d6c005dd330d36b2b4a2068626db7d09a1c18e5a05c"}, + {file = "uharfbuzz-0.56.0-cp310-abi3-win32.whl", hash = "sha256:45413c77ffdcec7ba94a3e6b8c3af384a28688dafee4bbf343afb85249af3f9a"}, + {file = "uharfbuzz-0.56.0-cp310-abi3-win_amd64.whl", hash = "sha256:0e7c628f327ce862a8606de93766f001704a5201e97d598686730ef2c1605c03"}, + {file = "uharfbuzz-0.56.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c4507b64a4c53fe9af89a47efbacffd707b20bb8afb3714f970390cd4319e476"}, + {file = "uharfbuzz-0.56.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4d5885586ccf247dff92033fdc15988c4c4931406e0faa445d0c6f0941061dc4"}, + {file = "uharfbuzz-0.56.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c707baaeabf3d90ae3fde6e12cdfa05a3f77b754aaa4b930a527aaf9d165d0e"}, + {file = "uharfbuzz-0.56.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84aafa43bf29729d839ae7eabdbbfe86bea2b3a8594195231b79b94a7f5be07d"}, + {file = "uharfbuzz-0.56.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d8768c480c1474f856da26a2e7195ca24c25fdb08e71a8d2ea30b50a413df1a7"}, + {file = "uharfbuzz-0.56.0.tar.gz", hash = "sha256:77f4ad1c9f32f44cc6f0c0c4f98fab54587719c96c810a043d01669f704d3e0c"}, +] + [[package]] name = "ujson" -version = "5.12.1" +version = "5.13.0" description = "Ultra fast JSON encoder and decoder for Python" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "ujson-5.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71bdb5d10c6d7e710cfa78e743d9fb79a37c7c66fa916cd287bffbaa520f5abe"}, - {file = "ujson-5.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:558673c6c3a2309775683ca96d5f1e4cd99889f71b1ba5cb6be8aa37ae67f9e0"}, - {file = "ujson-5.12.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4b0c9f6a56aa94bb98b403e1f57a866f0b43abaa89757b24d4a4b3cd8643ced"}, - {file = "ujson-5.12.1-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:7bba5ab7965619db7d6f5503133b8e2d8bfce9bb6754224ca64d19261cc52f7c"}, - {file = "ujson-5.12.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:191d2077fd53441599a2efd3dcc205b9cc5f3a4d685a76e9f73f4b6c19aee0c9"}, - {file = "ujson-5.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d90d27953716ef206c42f166932b3dbb264dc638bbf32acae81b216ae35f566d"}, - {file = "ujson-5.12.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b6afa86c117b66034004ee83c5149c6dccf7cb88941f9d3a1640c7076577f2d4"}, - {file = "ujson-5.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9523d67d45334f9a1d62e423bd72be62b58d2289a50420ffffa9363763eab73f"}, - {file = "ujson-5.12.1-cp310-cp310-win32.whl", hash = "sha256:757f2026bef09d231d63a2250a2c7ad21ea1c9cb1ded6480659d202c4e2ef09e"}, - {file = "ujson-5.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:7e31afad20cd6837a5ac6965d95b44b0ff06e42a82b01a8d3dc606a07f0b7a2a"}, - {file = "ujson-5.12.1-cp310-cp310-win_arm64.whl", hash = "sha256:80f58ae2be100da0f525330ee274accd8892d1c125fea75076f60539d9a5f9cd"}, - {file = "ujson-5.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26dcb43869057373048cbd2678293c5b0f962d5774cc76fc9488564a209bcbf2"}, - {file = "ujson-5.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bca3f04b2f590a8211acdc3ca06649b65a7ed1e999437dccf095310be9d3ba4e"}, - {file = "ujson-5.12.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29d1d64ed2c3c17666f4f0e15462800f3477255dc53667ad5d099277866c5666"}, - {file = "ujson-5.12.1-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:2cfbd6b0c677d5d053964b8f98d8bb1af10c591c8c24454bcd40006ac8ba18db"}, - {file = "ujson-5.12.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f75caed5b6d1fc271bb720a780c4199914267f7b865f9bf17826c4feccea582c"}, - {file = "ujson-5.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b21b4c680594c8686bcd4cdda0fd3ea2567b9d42bcf1d1e3d92d39bcdb02e8f1"}, - {file = "ujson-5.12.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50d07e79ec70d32b4fbe18ab706ed0b172be08710d5901b9d067d7951bfaa164"}, - {file = "ujson-5.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:080bc65ac7c0a6314d45d55b6171d3a48b1aeaf89895654d625b291cfe46309f"}, - {file = "ujson-5.12.1-cp311-cp311-win32.whl", hash = "sha256:251ba8229e19b4b0b3efb5e7e3ddfa67c5c466aa492707bc3f6568bf714604dc"}, - {file = "ujson-5.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:46315b82505c99101dcab3bd979f15fecfde85c02df7efbb4e428fa357665290"}, - {file = "ujson-5.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:12e99e49c62322ed0394c914aff15403ba7ede0b74f05a0faa4ec12c7d17a139"}, - {file = "ujson-5.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10f44bd08ae52ee23ca6e8b472692e5da1768af2d53ff1bad6f40b532e0bc7ee"}, - {file = "ujson-5.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cc6ea753b7303fa5629fa9ac9257ea4b001c4d72583b2bb36ff1855a07db49f"}, - {file = "ujson-5.12.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:289f13095764d03734adfa10107da9b530ceb64dc1b02a5f507588d978d5b7df"}, - {file = "ujson-5.12.1-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:427893168d074e59214b0ee058337c57f5bb80175cdd5b4799a9c931aae22022"}, - {file = "ujson-5.12.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7a81724d5d90a2da7155d15d8b156ce57eaed7cdd622df813f36a8e612fd4c8"}, - {file = "ujson-5.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a6efff7dc6515416366819de4a1bc449b77107c5b48508b101fd40f7f8bec08"}, - {file = "ujson-5.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77a71fe53427a0cf49d56eafd801d9f7e203b784b7f99cc717783fd6f6f7b732"}, - {file = "ujson-5.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ea3bed53d2ea8e5642e814a9e41f3e29420a8067874ba03ace8c0462e160490c"}, - {file = "ujson-5.12.1-cp312-cp312-win32.whl", hash = "sha256:758e5c8fbe4e6d483041e03b307b01fb5d2f2dd4452d4d4b927ab902e188939e"}, - {file = "ujson-5.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:f6074d3d3267ba1914c624b6e1fa3d8152648ff36b0ab77ddf83b92db488c30d"}, - {file = "ujson-5.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:7642a41520ac1b2bc25ea282b66b8da522cc43424442e6fb5e039be4d4f96530"}, - {file = "ujson-5.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c4bdc052a5d097f0a2e56d93aed97355f9f7a62ef9baa4f8517e43245434af9c"}, - {file = "ujson-5.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5dc91fa06ea35920b704fd9d70871897680145998071cfbf5ee3e19f2c9fc242"}, - {file = "ujson-5.12.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5db0849c0e3da54822a5834f2dc51d7c51072d7f7d665014ee34600dc10889b"}, - {file = "ujson-5.12.1-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:949cb4863a5d4847edeb47c5364b334e8cadf23a7cbdaa547d86098a4b093106"}, - {file = "ujson-5.12.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aa731138d6dfca4ab84501b72384e6c544bfb48cb87a0dd4d304df3246cac25"}, - {file = "ujson-5.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:727e983ef27892d86ee2d28fd517eeb02b2c1165aafcbe929dce988aeee81bfe"}, - {file = "ujson-5.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d57d731ecf492d3d011e65369f8330654f0875b19f646be5270d478e843d3b81"}, - {file = "ujson-5.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a09636220f26c66f80c6c6283023cb53120e843825f890be92696cd1aa43f39"}, - {file = "ujson-5.12.1-cp313-cp313-win32.whl", hash = "sha256:ee83fbac03a0896faf190177c938f94eb610b798d495a19d50997242c4eca685"}, - {file = "ujson-5.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:e08d9e096c416ddc34519241f97c201258b42639f2012d9547d8ae32921800dd"}, - {file = "ujson-5.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:963287e4b1bc463735c4056968a2dfa59bb831b6daba68bddd14f451191fe9e5"}, - {file = "ujson-5.12.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f19e9a407a24230df0cc1ec1c0f5999872ba526b14a780f80ad6479f5eed9bc"}, - {file = "ujson-5.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b657e870c77aaacdeea86cfad3e6d2ef9b52517e45988c9c367f7ee764fe4dd"}, - {file = "ujson-5.12.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984b5a99d1e0a037c2046c3c4b34cec832565d62d5017be0a035bf3cbfab72dc"}, - {file = "ujson-5.12.1-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:f48ef8a16f1d85bd7982beac7adfd3fb704058631db84c1c61c8a1b7072b1508"}, - {file = "ujson-5.12.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f39ba3b65cc637b59731532f7e7c807786bff1d0332ab2d5b96a04d2584d78f"}, - {file = "ujson-5.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:07f307780f85b49cba93f291718421b6f5f3b627a323b431fad937a18f6587cb"}, - {file = "ujson-5.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c335caea51c31494e514b82d50763b9792d3960d2c7d9fdb6b6fb8ed50ebdd0"}, - {file = "ujson-5.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ea07e29a45d199f926aadf93a9974128438c01b83141fba32477c0ee604b33"}, - {file = "ujson-5.12.1-cp314-cp314-win32.whl", hash = "sha256:c8e626b6bc9bdd2e8f7393b7d99f3daa2ca4022e6203662e70de7bb3604b21b9"}, - {file = "ujson-5.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:c6d3bdd020333688ee60559437021ed68a98a28fdd609b5af16de5dd58f90cba"}, - {file = "ujson-5.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:e3c9c894971f4ada3ded16a804ed4640e1f2b3e5239beaeec7c48296f39f4232"}, - {file = "ujson-5.12.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:49dd9c378e1c8e676785ff2b62cb490074229f15ab54abf45b623713cb2c36b5"}, - {file = "ujson-5.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d8827904358d7da59ccf2e1fd8de59e78248036d17fecc0462e62c6721f1102"}, - {file = "ujson-5.12.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc26caebea90425662ef0b979f945f6ac832651881107d6ec9a3c4d4a4ba929c"}, - {file = "ujson-5.12.1-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:45022aae09ac3d45bda6fbfc631088d1aff9a0465542d40bd6d295ced378c430"}, - {file = "ujson-5.12.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b22aa0f644516d3d5b29464949e4b23fe784f84b4a1030ab9ac3cb42aaedabb1"}, - {file = "ujson-5.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7dc5cf44ea42365cd1b66e6ed3fc6ca040c86587b024a6659b98e99d31cff2cd"}, - {file = "ujson-5.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8df5d984ff4ac1ef292d70f30da03417038a7e1e0bc272d28ca9d34f02f41682"}, - {file = "ujson-5.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:485f0182a0c0b54c304061cdc826d8343ce595c4055f7a24e72772a8520e5f7b"}, - {file = "ujson-5.12.1-cp314-cp314t-win32.whl", hash = "sha256:4e12ca368b397aed7fa1eec534ea1ba8d94977b376f9df3e93ae1acfd004ec40"}, - {file = "ujson-5.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:cec6b9b539539affc1f01a795c99574592a635ce22331b64f2b42e0af570659e"}, - {file = "ujson-5.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:696224d4cfb8883fa5c0285dff31e5ce924704dd9ccd38e9ea8b5bf4a42b12fc"}, - {file = "ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c419bf42ae40963fc27f70c59e24e9a97f5cf168dbce2c572f3c0ce3595912"}, - {file = "ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0be2b4f2f547b9f0f3d902640e410e5a2fc851576cbe033c88445a23e3e7aef1"}, - {file = "ujson-5.12.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:4ea0c490c702c20495e97345acfcf0c2f3153e658ef537ff111929c48b89e10a"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3e30fa6bc7156ed709e13f8b52e917db08fbfd611ba61346b62630974ec0ba8e"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f67c5f0d64eba0fbbd6d2d6a79b0c43c5bc06f27564378fd5d716e0d40360068"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8416bb724db9accfa97bdb77245952494b1800c23e42defd46afb5c661c9af19"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:66005b49c753a1b9f2f8853919dc58e1e6bd66846ea341a33afa76c6d7602485"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdc6b277dcd27663f7fb76b6a5088424c66e0407c23e9884f80cd733f7d71b19"}, - {file = "ujson-5.12.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7957b64583793042521f7f7c71c01626b3d32a17528eaab980eb8cdc3d4eec68"}, - {file = "ujson-5.12.1.tar.gz", hash = "sha256:5b7e96406c301a1366534479a7352ec40ec68bb327c0c119091635acd5925e35"}, + {file = "ujson-5.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:770643b4752266c5a466149848b78c3874940926a4ecef304f518b2b6cdb432f"}, + {file = "ujson-5.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8e8c1203cb1a27720debc334f840a9170da741503522f86999710cb4738fbe3"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:65c1813fdd742fe3c249d9c417fa490e5b54e8a91bf343a88486ec50d175c444"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5aa9bf16f0131812720dd4ae70bc1a0cf68f79e93c07c66100d328e75944b567"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82c17b904c03c2b9629486ec91a8fa46a15f10d03504284f54ed7257a917d9f1"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0b5f6983b2469db00e540b68fa8297b7a0ccd0d5173c60cc3e84f336b09395f"}, + {file = "ujson-5.13.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b877fe7926107d25d54b655c5b8dd94b294b22c157233163ad29fdb54ac10cf4"}, + {file = "ujson-5.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f3cae1c811e787b9500e2830af8632dcb32a78dea5baf15aac51d681c59a59dd"}, + {file = "ujson-5.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:820b78a6a183ab6591b2ea888020032ef0fe328d852af9a5c8d8084ababb2218"}, + {file = "ujson-5.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1209c985d1f4c2ae085ced7325509650cdb3533bb7294558dd1f26d48377942"}, + {file = "ujson-5.13.0-cp310-cp310-win32.whl", hash = "sha256:326553ae6c063c8246974906a6c137a0780fe46d143abebc52cd2cadda0f1814"}, + {file = "ujson-5.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:0354d6b50b0d153ed7c629845b18a953d0c727b7e768fd94a94e0602abfa1f29"}, + {file = "ujson-5.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:7b4a05f61a96553995da6a4d502e07bc8aa5d07a90031df006239c6b00ce1c83"}, + {file = "ujson-5.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b7badefa73f96bad9e295ea22bd06967b851c8aad68c74196437e3584f25de5"}, + {file = "ujson-5.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:09effd42924a80df20a63b31a1ede905e66b0ce24aafe7a4cbedb05c783f8bb3"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:89d0bfc986d02b4ce76b00e0f560bc8d30dfe8c05a1bfd8529e085eb6c1a77d9"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cdd9618e07b3b142a02f0ab8227fd52453688b8e8e60ac0511f13a25fa8009db"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0806683e8171ec06817e6af22f14ba0cd2f16def8a2ffb22a28a10615249355d"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1cf46c79498c81f088cad4165b1669a78bba7bfbeb778c7cc1aff316e062b0d"}, + {file = "ujson-5.13.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2282039eb26f08ed1a1381360395f8a310f39a45ef7314cb0f258a7d2917ea3"}, + {file = "ujson-5.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c75eb7fac0fe92925b959cf2fa18f88d9fb76b10781fd2a6ccb895d5fb89171"}, + {file = "ujson-5.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:12078e81def2790140583abefc1b979f3c77be15d53c524bf0e232f669822052"}, + {file = "ujson-5.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12192a37c6c0e476554b62647acdf6139a47b6f13d8bad8dd2316763c4cee12"}, + {file = "ujson-5.13.0-cp311-cp311-win32.whl", hash = "sha256:ae53b3f046529c193d533ca8111492330b204d6007611cdfa20e8b764c7c1389"}, + {file = "ujson-5.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:2275bbaaea3eddd2e8ec0863e28a420f5f520b14a760fc3f1e49fd07a974448c"}, + {file = "ujson-5.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:32a59e7151fe2fec8fdf9a565ee66fcf87918d48827e21cdab5e15ff9f274b78"}, + {file = "ujson-5.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf81570ac056cb058f9117b52ca5dd800bfe9381d0076d0bb30a08a54591d654"}, + {file = "ujson-5.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7edf16359c52ed53406e216565d83e6b98c23c3cb9a0a01673f2493f8fb15edf"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:24539618fb3243cfdf27dab9a850acab80798a01501e9586b61fb9ecd016a891"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fdde6341d213b29f413b5fa9fad1392d5408074c75f0900ed949e97e546fa5df"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:229faf041ef249ee3fd57bac1cedb123d2718ab63f6ccd50eca95ea902eb0dca"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d02f31c2f59cc6a1c2c3633b377701fc2d8e876cc01950735d7a01132ccc233"}, + {file = "ujson-5.13.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea7204e9fa7538bfbb1396e1ee8c2bbcd3818b3633ef5bb14d4fdea52994d14d"}, + {file = "ujson-5.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c5a2478a3a1fa4421f7e035b87194eea0cf44c7971a3f32ad1b42a0dfd63c03"}, + {file = "ujson-5.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b535e0970c96957e999cfe5ec89361f0e8d0bb987fb5d5144f6f495cb3ed9e19"}, + {file = "ujson-5.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d0ad1207694988498fca7e0bb28eba7564fa33261d2f9fdf66a3aaab376b803"}, + {file = "ujson-5.13.0-cp312-cp312-win32.whl", hash = "sha256:d6bc9fa43a49e403c68c7eb164eef0feee9dd29474a7c6e0d3b6267025371990"}, + {file = "ujson-5.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6692d49ff970aaa5008f4a6fe06974bc91fd957bf13173f765e46d8ba44906ea"}, + {file = "ujson-5.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:5737ffe0740a788b0e6255f0ffb281db49305fd6e6a587be44c73d9e92b554c4"}, + {file = "ujson-5.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46998fc8d11aec34a20e2010905e7059732a3d192d9a3c3fe4f9ffd146c87ec8"}, + {file = "ujson-5.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ee03ce288ba25b05cf0de87203165642277a25caa4f00a437e13152e5214e310"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:cdf33b588a81b05d0b585c66f83050c49cb670623424d10e4d1ad37ba2f7eed9"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cabd73c114ce93c21d7db2e2d8e16217fd8a5b2b3ec754629eebef5c262d47f"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ffc61fc756a64f4d169a78cc638d769e3c324f45fc51997626abf4e5e5dd6460"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c00323c13a35822c9a67a26c0b2a0787510bf1ef490922b58009b362d1a3e21"}, + {file = "ujson-5.13.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496e662a6b46d5f936d77fb68259cece19213bb2301ddd520dbd75ac7c90c5f4"}, + {file = "ujson-5.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7fd41b86444df14f8b4b7afaaa9f27bacfbf8c18380872317aeab6cd125dcede"}, + {file = "ujson-5.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bf3c2c4ea55d4187903fcdc689a9bf5b0fc72d8c0eaff39db18c1f337c8832c1"}, + {file = "ujson-5.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6eca7751d61045a9b1e7f9a8c97ac24b164f085b60bef1c4668654bb2338011"}, + {file = "ujson-5.13.0-cp313-cp313-win32.whl", hash = "sha256:b63d3820f978bc8e98cc3f1fe26a33b0d2ea237733a23fe5e9cb5d51f466bd97"}, + {file = "ujson-5.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:17a59d5cf23ef98f7c9314524976b4b288374d83200add01d953024fb06404f9"}, + {file = "ujson-5.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:b49516fbe803ff30d6caa9ccc3799ec7f968992747ce3099eae4758928577b53"}, + {file = "ujson-5.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc9dfd41fed397ab03bb9d9fe1cbd83301211c772a17536033ce7d68877ac82b"}, + {file = "ujson-5.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca7ef2fa6c408a7c0f558e4d33d93b32ddc35ed6d3cfc505747931a64b7465d5"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a554b2e5bee85030369514cef8b0b913cebe1a4c2c0c13541966d50bcba22b1a"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea939ff629ab03ae970d03eca6d1febd8ed55ba38ca44aec64ce997537cd3fa0"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b98bf2faa5e37ecfe752226ea08290031e375a0c43d425a0b955fb3e702a2a71"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a4b92344b16e414aeb609e57f62c466500e53c94f1698f5b149dc0b7223ec3e"}, + {file = "ujson-5.13.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df805aad707507a1fa165fb716218ca3a89f142125dc4b23c9fcc08fa402d97"}, + {file = "ujson-5.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7576bdbef327c3528f011002a2d74486f6fe4e33289bdb7a042b7f1a6e9d8285"}, + {file = "ujson-5.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6eee5d7cce3f32a468905f9ff61807a60287a90258d849460f6fa826e810870d"}, + {file = "ujson-5.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:144e9d8a454cfa727e0f755e1863738ed68068583bda5463052cb446835bd56c"}, + {file = "ujson-5.13.0-cp314-cp314-win32.whl", hash = "sha256:576f35c35b918d67d41b933878062ec0a5c9f4d1e9e14e04aeef35384963feae"}, + {file = "ujson-5.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:d5e206e9f849ead27e51ef8da44e52b38da7c6dbd929a7340ab44533edcda8d7"}, + {file = "ujson-5.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:dc470179775468f9a007d3a6a2734624248c94bf47c6645e808c7e50a5070d1a"}, + {file = "ujson-5.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:69b4e36bb7d5f413ba8c00c8006b2ec627cc5ace97301462f6aadb66ec9d2979"}, + {file = "ujson-5.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b644d50f66de5490c1823c7176618cead5e8e8a88cba9f40a6308ca52e79267"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:15107aaa4f559d55201165ec32abb35c283a861be1fa67229578cb7d93fcd93a"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e343c5f0c058523f1edbf6ae4eceb4e0d934205a53bbdd8d9a945c83324662a"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02200035bc80e830f076ffc1b329a94c295aee6d9de8c9043647cb9a7bd4f76f"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f19b81b73ff28f5c5022ee794f94122bfcda07a76423078e349465d71223a1"}, + {file = "ujson-5.13.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82e1393e6dbe3c95fdfc95c6c528890e191351a1f024ef51126cf1f22543af52"}, + {file = "ujson-5.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38afcf994b28ed85ea2420e2a8d79a37d0a77348b3daf53850c16edda66f942d"}, + {file = "ujson-5.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1bdf2518971586f2b413156c49d9dd8b56cc990a8647081e1bd00af60564d469"}, + {file = "ujson-5.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:751ad01042472f1c7c02f5c597c7aee79834e82a6cc384ca302173bbc8e8deb8"}, + {file = "ujson-5.13.0-cp314-cp314t-win32.whl", hash = "sha256:74f3dd61aeb01b7b2a6754e400224e819279041b3867935a55ccf57fb86a43b2"}, + {file = "ujson-5.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5c31317d5e4504dae8f98795358b6082fc0ef96e7394806db0a76a4a8717f446"}, + {file = "ujson-5.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aefd3c9c95f9b62348956396ff7b31818476f8f54dc4a4e64cbd4f0491db6fca"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3e074a1f7778d58aa3b3056bab7b6251aabb3f381808018ca2b7fb8dbdeef7ab"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bb53ef95d35875262b8d0aa28506ca612ddd07058bee2a90f609938e69dc801"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb296a0aa480ab88d895ddaa90372604c08ccc72323f02590612c775426ab413"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2862f81af44b3a7e74c5d80caa118d736be1991ce6f1d5c723716fa403060cc6"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c16e07581172f08585b409246f4535dab13ee85af0e3d3cfa8684b653ca44fa8"}, + {file = "ujson-5.13.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:9bd0f2dd05937c3b089af316884de18c6f6182ddb8ffce597d2e7c7a9ba9f447"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:96e7e019b097b4b25fccddadb369d13f412c13695fcf0680b6bf906376156151"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7892ea6dd85ede6d30fbbd22af1239b9d81dabdf9b7a8f10ca6d4464d4d9b8ab"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e1842e10adc8f0db0d3e8aa3a1f8b05ce0456b39e180c8553d7f36dd0bf24b6b"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8bbb1f5ab810954fb307b6c5e68af58210c31da8569f9e6498a3958c1859e72"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f6d55c68985654a84a9b47ac51f2655adac4fd264e4189f832960c2270dcf70"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b266f182d4bce74a6a9e1f86988485e8cd422efdcc7c3f537e00cc956f52678"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfaa8302eb9bb7f5e231f256caf83040760585e32751d19155c0f0c0225f8de1"}, + {file = "ujson-5.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:43dee3081b00fe447c5b9e2fe7ebfd57f7bcc5dd25b8a439c26c8c174dd581be"}, + {file = "ujson-5.13.0.tar.gz", hash = "sha256:d62e3d7625384c08082abad81a077af587fdef2761bb14c3822f4234b8d07d75"}, ] [[package]] @@ -7945,6 +8732,18 @@ files = [ {file = "Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23"}, ] +[[package]] +name = "uritemplate" +version = "4.2.0" +description = "Implementation of RFC 6570 URI Templates" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686"}, + {file = "uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e"}, +] + [[package]] name = "urllib3" version = "2.0.7" @@ -8315,6 +9114,25 @@ files = [ icalendar = "*" tzdata = "*" +[[package]] +name = "xdatatrees" +version = "0.1.3" +description = "An extension/wrapper for the `datatrees`/`dataclasses` library that adds powerful XML serialization and deserialization capabilities to your dataclasses." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "xdatatrees-0.1.3-py3-none-any.whl", hash = "sha256:fb06b57bb65a47af84d2f39201409b505a2055509c03a82dc68364aab6abdae0"}, + {file = "xdatatrees-0.1.3.tar.gz", hash = "sha256:126d5c3f00c179fabb12eb4b2cc1c09f82e40a2fccec47afcc7942add3cec32c"}, +] + +[package.dependencies] +datatrees = ">=0.3.6" +lxml = ">=4.6.0" + +[package.extras] +test = ["numpy (>=1.20.0)", "pytest (>=7.0.0)", "pytest-cov (>=4.0.0)"] + [[package]] name = "xlrd" version = "2.0.1" @@ -8571,6 +9389,18 @@ docs = ["sphinx (>=1.4.0)"] test = ["coverage[toml] (==5.2.1)", "flake8 (==3.8.3)", "flake8-blind-except (==0.1.1)", "flake8-debugger (==3.2.1)", "flake8-imports (==0.1.1)", "freezegun (==0.3.15)", "isort (==5.3.2)", "pretend (==1.0.9)", "pytest (==6.2.5)", "pytest-asyncio", "pytest-cov (==2.8.1)", "pytest-httpx", "requests-mock (>=0.7.0)"] xmlsec = ["xmlsec (>=0.6.1)"] +[[package]] +name = "zict" +version = "3.0.0" +description = "Mutable mapping tools" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "zict-3.0.0-py2.py3-none-any.whl", hash = "sha256:5796e36bd0e0cc8cf0fbc1ace6a68912611c1dbd74750a3f3026b9b9d6a327ae"}, + {file = "zict-3.0.0.tar.gz", hash = "sha256:e321e263b6a97aafc0790c3cfb3c04656b7066e6738c37fffcca95d803c9fba5"}, +] + [[package]] name = "zope-event" version = "6.2" @@ -8658,4 +9488,4 @@ bindings = ["ghostscript"] [metadata] lock-version = "2.1" python-versions = ">=3.12.10,<3.13" -content-hash = "0d71db12bc80937e64ea803826a57dea2d689b07fd250353c772f2a0d82989db" +content-hash = "7f1a4d558e493d667761a2a6cbe490ac7a01044a8dbb22c51999c5c79132db97" diff --git a/requirement/pyproject.odoo18.0_python3.12.10.toml b/requirement/pyproject.odoo18.0_python3.12.10.toml index b923af7..b881fdd 100644 --- a/requirement/pyproject.odoo18.0_python3.12.10.toml +++ b/requirement/pyproject.odoo18.0_python3.12.10.toml @@ -21,11 +21,12 @@ authors = [ "Mathieu Benoit ",] python = ">=3.12.10,<3.13" a2wsgi = ">=1.10.6" aiobotocore = "3.1.2" +altcha = ">=2.0.0" ansi2html = "^1.9.2" apispec = "^6.10.0" asn1crypto = "1.5.1" astor = "^0.8.1" -avalara = "^26.4.0" +avalara = "^26.7.3" babel = "2.10.3" bandit = "^1.9.4" base64io = "^1.0.3" @@ -33,7 +34,7 @@ beautifulsoup4 = "4.13.5" bokeh = "3.6.3" boto3 = "1.42.42" botocore = "1.42.42" -cachetools = "^7.1.1" +cachetools = "^7.1.7" cairosvg = "^2.9.0" caldav = "1.3.9" cbor2 = "5.6.2" @@ -43,21 +44,20 @@ code-writer = "^1.3.0" colorama = "^0.4.6" contextvars = "^2.4" cryptography = "46.0.5" -cssselect = "^1.4.0" -cython = "^3.2.4" -dateparser = "^1.4.0" +cssselect = "^1.5.0" +cython = "^3.2.9" +dateparser = "^1.4.2" decorator = "5.1.1" dnspython = "^2.8.0" docutils = "0.20.1" -dropbox = "^12.0.2" +dropbox = "^12.2.1" easypost = "7.15.0" email-validator = "^2.3.0" extendable = ">=0.0.4" extendable-pydantic = ">=1.2.0" -extract-msg = "^0.55.0" +extract-msg = "^0.56.0" extruct = "^0.18.0" -ezdxf = "^1.4.3" -factur-x = "^4.2" +ezdxf = "^1.4.4" fastapi = ">=0.110.0" filetype = "^1.2.0" flake8 = "^7.3.0" @@ -65,9 +65,9 @@ formio-data = "^2.1.7" freezegun = "1.2.1" geocoder = "^1.38.1" geoip2 = "2.9.0" -geojson = "^3.2.0" +geojson = "^3.3.0" gevent = "24.2.1" -gitpython = "^3.1.50" +gitpython = "^3.1.59" giturlparse = "0.12.0" googletrans-py = "4.0.0" greenlet = "3.0.3" @@ -76,36 +76,38 @@ icalendar = "5.0.13" idna = "3.6" img2pdf = "^0.6.3" isort = "7.0.0" +itsdangerous = "^2.2.0" jinja2 = "3.1.2" js2py = "^0.74" libsass = "0.22.0" lottie = "^0.7.2" lxml = "5.2.1" -lxml-html-clean = "^0.4.4" -mako = "^1.3.12" +lxml-html-clean = "<0.4.5" +mako = "^1.4.1" markdown2 = "2.5.1" markdownify = "0.13.1" markupsafe = "2.1.5" -matplotlib = "^3.10.9" +matplotlib = "^3.11.1" meteostat = "1.6.8" mock = "^5.2.0" mpld3 = "0.5.10" -msgraphfs = "^0.4" +msgraphfs = "^0.5" mysqlclient = "^2.2.8" nextcloud-api-wrapper = "^0.2.3" num2words = "0.5.13" numpy = ">=1.15" numpy-financial = "<=1.0.0" -numpy-stl = "^3.2.0" +numpy-stl = "^4.0.0" oauthlib = "^3.3.1" +odoo-test-helper = "^2.1.3" odoorpc = "^0.10.1" ofxparse = "0.21" ollama = "^0.6.2" -openai = "^2.34.0" +openai = "^2.53.0" openpyxl = "3.1.2" -openupgradelib = "^3.13.1" +openupgradelib = "^3.13.6" openwebui-client = ">=0.3.0" -orjson = "^3.11.8" +orjson = "^3.11.9" ovh = "^1.2.0" pandas = "2.3.3" paramiko = "<4.0.0" @@ -115,14 +117,14 @@ pathspec = "^1.1.1" pdf2image = "^1.17.0" pdfminer-six = "20260107" pexpect = "^4.9.0" -phonenumbers = "^9.0.29" +phonenumbers = "^9.0.36" pillow = "12.2.0" -plaid-python = "^39.2.0" -plotly = "^6.7.0" +plaid-python = "^42.0.0" +plotly = "^6.9.0" polib = "1.1.1" -pre-commit = "^4.6.0" +pre-commit = "^4.6.2" premailer = "^3.10.0" -prometheus-client = "^0.25.0" +prometheus-client = "^0.26.0" psutil = "5.9.8" psycopg2 = "2.9.9" py3dns = "^4.0.2" @@ -135,10 +137,9 @@ pydevd-odoo = "^1.2.2" pyftpdlib = "^2.2.0" pygithub = "^2.9.1" pyjsparser = "^2.7.1" -pyjwt = "^2.12.1" -pylint = "^4.0.5" -pylint-odoo = "^10.0.2" -pymupdf = "^1.27.2.3" +pyjwt = "^2.13.0" +pylint = "^4.0.7" +pylint-odoo = "^10.0.9" pyncclient = "^0.7" pyopenssl = "26.0.0" pypdf = ">=3.1.0" @@ -154,13 +155,13 @@ python-dotenv = ">=1.0.0" python-jose = "^3.5.0" python-json-logger = "^4.1.0" python-ldap = "3.4.4" -python-multipart = "^0.0.27" +python-multipart = "^0.0.32" python-pptx = "^1.0.2" python-resize-image = "^1.1.20" python-slugify = "^8.0.4" python-stdnum = "1.19" -python-telegram-bot = "^22.7" -pytz = "^2026.2" +python-telegram-bot = "^22.8" +pytz = "^2026.3.post1" pyusb = "1.2.1" pyyaml = "^6.0.3" pyzbar = "^0.1.9" @@ -168,27 +169,28 @@ pyzk = "^0.9" qifparse = "^0.5" qrcode = "7.4.2" radon = "^6.0.1" -redis = "^7.4.0" -regex = "^2026.4.4" +redis = "^8.1.0" +regex = "^2026.7.19" reportlab = "4.1.0" -requests = ">=2.31.0" +requests = "2.31.0" requests-oauthlib = "^2.0.0" requests-toolbelt = "^1.0.0" -responses = "^0.26.0" +responses = "^0.26.2" rjsmin = "1.2.0" roulier = "^1.1.1" -s3fs = "2026.4.0" +s3fs = "2026.7.0" schwifty = "2024.4.0" sentry-sdk = ">=2.0.0,<=2.22.0" setuptools = "80.10.2" shapely = "^2.1.2" -sqlalchemy = "^2.0.49" +sqlalchemy = "^2.0.51" statsd = "^4.0.1" -tldextract = "^5.3.1" -tweepy = "^4.16.0" -twilio = "^9.10.5" -typing-extensions = "^4.15.0" -ujson = "^5.12.1" +tldextract = "^5.3.2" +to-3mf = "^0.1.0" +tweepy = "^4.17.0" +twilio = "^9.10.9" +typing-extensions = "^4.16.0" +ujson = "^5.13.0" unidecode = "^1.4.0" urllib3 = "2.0.7" uvloop = "^0.22.1" @@ -204,6 +206,18 @@ xlwt = "1.3.0" xmltodict = "0.13.0" zeep = "4.2.1" +[[tool.poetry.dependencies.pymupdf]] +version = "^1.28.2" +markers = "platform_machine != \"s390x\"" + +[[tool.poetry.dependencies.factur-x]] +version = ">=4.2" +markers = "platform_machine != \"s390x\"" + +[[tool.poetry.dependencies.factur-x]] +version = "<4" +markers = "platform_machine == \"s390x\"" + [tool.poetry.dependencies.black] git = "https://github.com/psf/black.git" rev = "24.8.0" @@ -214,7 +228,7 @@ rev = "update_imghdr_python3" [tool.poetry.dependencies.fsspec] extras = [ "s3",] -version = "^2026.4.0" +version = "^2026.7.0" [tool.poetry.dependencies.pysaml2] git = "https://github.com/prauscher/pysaml2.git" diff --git a/requirement/requirements.odoo18.0_python3.12.10.txt b/requirement/requirements.odoo18.0_python3.12.10.txt index 2959e9f..bc7b2cb 100644 --- a/requirement/requirements.odoo18.0_python3.12.10.txt +++ b/requirement/requirements.odoo18.0_python3.12.10.txt @@ -78,7 +78,26 @@ git+https://github.com/prauscher/pysaml2.git@replace-pyopenssl pandas==2.3.3 pillow==12.2.0 -s3fs==2026.4.0 +# OCA_storage amène « fsspec[s3] » sans borne : l'extra s3 de fsspec 2026.7.0 +# exige s3fs>=2026.6.0, incompatible avec l'épingle 2026.4.0. On aligne s3fs sur +# fsspec ; aiobotocore 3.1.2 reste dans la plage exigée (>=2.19.0,<4.0.0), donc +# le trio botocore/boto3 n'a pas à bouger. +s3fs==2026.7.0 aiobotocore==3.1.2 botocore==1.42.42 boto3==1.42.42 + +# factur-x >= 4.0 dépend de saxonche, qui encapsule SaxonC. Saxonica ne publie +# de roue que pour x86_64, aarch64, macOS et Windows — aucune pour s390x, et +# aucune distribution source : rien à installer, rien à compiler. La cause est +# en amont, GraalVM Native Image ne ciblant pas s390x. +# La 3.x n'exige que lxml et pypdf. On y retombe là où saxonche n'existe pas, +# au prix de la validation Schematron. +factur-x>=4.2 ; platform_machine != 's390x' +factur-x<4 ; platform_machine == 's390x' + +# lxml-html-clean 0.4.5 (2026-05-20) exige lxml>=6.1.1, alors qu'Odoo 18 épingle +# lxml==5.2.1 : le solveur poetry n'a plus de solution. Les versions jusqu'à +# 0.4.4 n'imposent aucune borne sur lxml — vérifié sur PyPI, release par +# release. À lever le jour où lxml montera. +lxml-html-clean<0.4.5 diff --git a/script/poetry/poetry_add_build_dependency.sh b/script/poetry/poetry_add_build_dependency.sh index 6c1cfa2..3aedc13 100755 --- a/script/poetry/poetry_add_build_dependency.sh +++ b/script/poetry/poetry_add_build_dependency.sh @@ -2,12 +2,29 @@ Red='\033[0;31m' # Red Color_Off='\033[0m' # Text Reset -./.venv.$(cat ".erplibre-version" | xargs)/bin/poetry add -vv $(grep -v ";" ./.venv.$(cat ".erplibre-version" | xargs)/build_dependency.txt | grep -v "*" ) -# poetry add -vv $(grep -v ";" ./.venv/build_dependency.txt | grep -v "*" | sed 's/==/@^/' ) -# poetry export -f ./.venv/build_dependency.txt --dev | poetry run -- pip install -r /dev/stdin +VENV="./.venv.$(cat ".erplibre-version" | xargs)" +BUILD="${VENV}/build_dependency.txt" + +# Chaque ligne devient UN argument, jamais plusieurs. +# +# L'expansion « $(...) » non quotée découpait sur les espaces : une ligne +# portant un marqueur — « factur-x>=4.2 ; platform_machine != "s390x" » — +# arrivait à poetry en cinq arguments. D'où le « grep -v ';' » historique, qui +# écartait purement et simplement ces lignes : la dépendance n'était jamais +# ajoutée. Or « poetry add » sait lire une chaîne PEP 508 complète, marqueur +# compris — vérifié avec son propre parseur. +# +# On garde le filtre sur « * » : une contrainte joker n'apporte rien à poetry. +mapfile -t DEPS < <(grep -v '\*' "${BUILD}" | grep -v '^[[:space:]]*$') + +if [[ ${#DEPS[@]} -eq 0 ]]; then + echo "Aucune dépendance à ajouter depuis ${BUILD}." + exit 0 +fi + +"${VENV}/bin/poetry" add -vv "${DEPS[@]}" retVal=$? if [[ $retVal -ne 0 ]]; then echo -e "${Red}Error${Color_Off} ./script/poetry/poetry_add_build_dependency.sh" -# cat ./.venv/build_dependency.txt exit 1 fi diff --git a/script/poetry/poetry_update.py b/script/poetry/poetry_update.py index e918885..1fa1339 100755 --- a/script/poetry/poetry_update.py +++ b/script/poetry/poetry_update.py @@ -191,6 +191,9 @@ def combine_requirements(config): # remove comments at the end of module b = b[: b.index("#")].strip() comment_depend = "" + # Défini pour TOUTE ligne : il est relu plus bas, y compris + # pour les lignes sans marqueur. + keep_marker = "" if except_sign in b: # current_python_version = platform.python_version() # current_platform = sys.platform @@ -205,9 +208,20 @@ def combine_requirements(config): config.set_version_python ) req = Requirement(b) - is_compatible = req.marker.evaluate(current_platform_2) - if not is_compatible: - continue + # Le nom de l'artefact généré — pyproject.odooXX_pythonYY — + # fige la version d'Odoo et celle de Python, PAS + # l'architecture. Évaluer « python_version » ici est donc + # juste ; évaluer « platform_machine » y graverait + # silencieusement l'architecture de la machine qui génère, + # et le lock produit sur x86 devient faux sur s390x. + # Un tel marqueur est donc CONSERVÉ et transmis à Poetry, + # qui sait résoudre les deux branches dans un seul lock. + if "platform_machine" in str(req.marker): + keep_marker = str(req.marker) + else: + is_compatible = req.marker.evaluate(current_platform_2) + if not is_compatible: + continue # this support python_version into comment_depend, check odoo/requirements.txt # b, comment_depend = b.split(except_sign) # b = b.strip() @@ -215,6 +229,26 @@ def combine_requirements(config): # print(comment_depend) # Rewrite dependancy : module==version b = req.name + str(req.specifier) + if keep_marker: + b += f" ; {keep_marker}" + + # Deux lignes du même module portant des marqueurs DIFFÉRENTS + # ne sont pas un conflit : elles s'excluent mutuellement. La + # clé inclut donc le marqueur, sans quoi la détection de + # versions divergentes les opposerait et en éliminerait une. + # + # Le nom vient de PEP 508 et non d'un découpage : avec un + # marqueur, le premier opérateur de la ligne est souvent celui + # du marqueur (« != »), et le découpage rendrait + # « factur-x>=4.2 » comme nom de module — que la liste des + # ignorés ne retrouverait plus. + if keep_marker: + module_name = f"{req.name} ; {keep_marker}" + dct_requirements[module_name].add(b) + dct_requirements_module_filename[b].append( + str(requirements_filename) + ) + continue # Regroup requirement for sign in lst_sign: @@ -258,6 +292,22 @@ def combine_requirements(config): config, dct_requirements, lst_sign ) + # Un module dont les requirements donnent des variantes par marqueur ne doit + # pas recevoir en plus une entrée NUE. Les manifest.py des addons déclarent + # « factur-x » sans version ni marqueur : cette entrée s'ajouterait aux deux + # branches et poetry recevrait trois contraintes pour le même paquet, dont + # une sans condition — ce qui annulerait la distinction par architecture. + marked_names = { + key.split(";")[0].strip() for key in dct_requirements if ";" in key + } + for name in marked_names: + if name in dct_requirements: + del dct_requirements[name] + print( + f"{Fore.YELLOW}WARNING{Style.RESET_ALL} - '{name}' est déjà" + " décliné par marqueur : l'entrée sans condition est ignorée." + ) + # Merge all requirement by insensitive dct_requirement_insensitive = {} copy_dct_requirements = dct_requirements.copy() @@ -418,8 +468,13 @@ def combine_requirements(config): lst_ignore, ignore_requirement_file = get_list_ignored() lst_ignored_key = [] for key in dct_requirements.keys(): + # La clé n'est pas toujours un nom nu : selon la branche qui l'a créée + # elle porte un marqueur (« paquet ; platform_machine … ») ou une + # version (« pymssql<=2.2.5 »). La liste d'ignorés ne nomme que le + # module, on ramène donc la clé à son nom avant de comparer. + bare_key = re.split(r"[\[<>=!~;]", key)[0].strip() for ignored in lst_ignore: - if ignored == key.strip(): + if ignored == bare_key: lst_ignored_key.append(key) for key in lst_ignored_key: del dct_requirements[key] @@ -514,6 +569,68 @@ def get_manifest_external_dependencies(config, dct_requirements, lst_sign): return dct_requirements +def apply_marker_variants(pyproject_filename, build_dependency_filename): + """Rétablit les paquets déclinés en plusieurs branches par marqueur. + + « poetry add » déduplique par nom : des deux branches de factur-x, seule la + dernière survit, et l'architecture qui n'est pas la sienne hérite de la + mauvaise version. Poetry sait pourtant exprimer le cas — mais uniquement + dans le pyproject, en liste de contraintes, hors de portée du CLI. On la + reconstruit depuis build_dependency.txt, qui porte toutes les branches. + + :return: True si le pyproject a été réécrit, donc si le lock est à refaire + """ + dct_variant = defaultdict(list) + with open(build_dependency_filename, "r") as f: + for line in f: + line = line.strip() + if not line or ";" not in line: + continue + req = Requirement(line) + dct_variant[req.name].append( + {"version": str(req.specifier), "markers": str(req.marker)} + ) + dct_variant = {k: v for k, v in dct_variant.items() if len(v) > 1} + if not dct_variant: + return False + + with open(pyproject_filename, "r") as f: + dct_pyproject = toml.load(f) + dependencies = ( + dct_pyproject.get("tool", {}).get("poetry", {}).get("dependencies") + ) + if not dependencies: + return False + + has_changed = False + for name, lst_branch in dct_variant.items(): + key = next( + (k for k in dependencies if k.lower() == name.lower()), None + ) + if not key: + continue + dependencies[key] = lst_branch + has_changed = True + str_branch = ", ".join( + f"{d['version']} si {d['markers']}" for d in lst_branch + ) + print(f"Décline '{key}' par marqueur : {str_branch}.") + + if has_changed: + with open(pyproject_filename, "w") as f: + toml.dump(dct_pyproject, f) + return has_changed + + +def call_poetry_lock(config): + """ + :return: True if success + """ + venv_dir = f".venv.{config.set_version_erplibre}" + status = os.system(f"./{venv_dir}/bin/poetry lock") + return status == 0 + + def call_poetry_add_build_dependency(): """ @@ -570,6 +687,15 @@ def main(): status = call_poetry_add_build_dependency() if status and pyproject_toml_filename: sorted_dependency_poetry(pyproject_toml_filename) + build_dependency_filename = ( + f"./.venv.{config.set_version_erplibre}/build_dependency.txt" + ) + if apply_marker_variants( + pyproject_toml_filename, build_dependency_filename + ): + # Le lock écrit par « poetry add » ignore les branches qu'on + # vient de rétablir : il ne décrit plus le pyproject. + status = call_poetry_lock(config) if ( config.force and not os.path.islink(poetry_default_lock_path) From 36fc4e319af9a7dc69ecb73543f76500b67e28e9 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 06:57:23 -0400 Subject: [PATCH 03/41] [FIX] tui qemu: the form crashed on the resource totals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "TypeError: unsupported operand type(s) for +: 'int' and 'NoSelection'" as soon as the custom resource profile was picked. Textual 8 turned Select.BLANK into a deprecated alias worth False, the sentinel having become Select.NULL. The guard compared against False and filtered nothing; NoSelection, lacking __bool__ and thus truthy, sailed through "or default" into the plan totals. The sentinel is now resolved at runtime, and both summed fields are coerced to a positive int inside the pure function -- where the invariant belongs, whatever Textual version is installed. --- FR --- « TypeError: unsupported operand type(s) for +: 'int' and 'NoSelection' » dès qu'on choisissait le profil personnalisé. Textual 8 a ramené Select.BLANK à un alias déprécié valant False, le sentinelle étant devenu Select.NULL. La garde comparait donc à False et ne filtrait plus rien ; NoSelection, dépourvu de __bool__ et tenu pour vrai, traversait « ou valeur par défaut » jusqu'aux sommes du plan. Le sentinelle est résolu à l'exécution, et les deux champs additionnés sont ramenés à un entier positif dans la fonction pure — c'est là que l'invariant appartient, quelle que soit la version de Textual. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 76df668..e9b929c 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -63,6 +63,19 @@ def disk_gb(value) -> int: return int(parsed[:-1]) if parsed else 0 +def positive_int(value, fallback): + """Entier strictement positif, sinon `fallback`. + + Ces valeurs viennent de widgets : une liste déroulante sans choix rend un + sentinelle, une saisie libre rend du texte, éventuellement vide. Aucun des + deux ne doit atteindre les totaux, qui les additionnent.""" + try: + number = int(value) + except (TypeError, ValueError): + return fallback + return number if number > 0 else fallback + + def apply_profile(entries, profile, base_vcpus, host_cpu, custom=None): """Applique le profil de ressources aux entrées choisies. @@ -74,9 +87,9 @@ def apply_profile(entries, profile, base_vcpus, host_cpu, custom=None): for e in entries: if profile == "custom": cus = custom or {} - ram = cus.get("ram") or e["ram"] - disk = cus.get("disk") or e["disk"] - vcpus = cus.get("vcpus") or base_vcpus + ram = positive_int(cus.get("ram"), e["ram"]) + disk = parse_disk(cus.get("disk")) or e["disk"] + vcpus = positive_int(cus.get("vcpus"), base_vcpus) else: mult = int(profile) ram = e["ram"] * mult @@ -213,6 +226,12 @@ def run_deploy_form(ctx, run_app: bool = True): Static, ) + # Textual 8 a ramené Select.BLANK à un alias déprécié valant False ; le + # sentinelle « rien de choisi » est Select.NULL. Comparer à BLANK ne + # filtrait donc plus rien, et NoSelection — dépourvu de __bool__, donc + # tenu pour vrai — passait pour une valeur jusque dans les totaux. + SELECT_NULL = getattr(Select, "NULL", Select.BLANK) + catalog = ctx["catalog"] arches = ctx["arches"] domains = set(ctx.get("domains") or []) @@ -403,7 +422,7 @@ def run_deploy_form(ctx, run_app: bool = True): ) yield Select( [(lbl, i) for i, (lbl, _c) in enumerate(profiles)], - value=0 if profiles else Select.BLANK, + value=0 if profiles else SELECT_NULL, allow_blank=not profiles, id="f_profile_install", ) @@ -580,7 +599,7 @@ def run_deploy_form(ctx, run_app: bool = True): mapping = {"f_vcpus": "vcpus", "f_ram": "ram", "f_disk": "disk"} field = mapping.get(event.select.id) if field: - if event.value is not Select.BLANK: + if event.value is not SELECT_NULL: self.custom[field] = event.value self._recompute() From 7650710295560cb6e0532fe4c837641d23c225c4 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 06:57:49 -0400 Subject: [PATCH 04/41] [ADD] tui qemu: free values for vCPU, RAM and disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI already took a typed value -- a letter picks a suggestion, a digit stands for itself. The form, though, locked the custom profile into its three dropdowns. Each now gets a final "free value..." entry revealing an input below it. An invalid entry overwrites nothing: the resource falls back to the catalog value instead of freezing the view. The 3 vCPU preset was missing too, between 2 and 4; it comes from the same constant, so both interfaces gain it together. --- FR --- La CLI acceptait déjà une valeur tapée — une lettre choisit une suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait le profil personnalisé dans ses trois listes déroulantes. Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une saisie sous elle. Une entrée invalide n'écrase rien : la ressource retombe sur celle du catalogue plutôt que de bloquer la vue. Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même constante, donc les deux interfaces l'offrent ensemble. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 86 +++++++++++++++++++++++++++++---- script/todo/todo.py | 2 +- script/todo/todo_i18n.py | 8 +++ 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index e9b929c..c49511a 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -245,6 +245,16 @@ def run_deploy_form(ctx, run_app: bool = True): result = {"spec": None} AUTO = "__auto__" + # Dernier choix de chaque liste de ressources : il ne porte pas de valeur, + # il révèle la saisie libre placée juste dessous. + FREE = "__free__" + RES_FIELDS = { + "vcpus": ("#f_vcpus", "#c_vcpus"), + "ram": ("#f_ram", "#c_ram"), + "disk": ("#f_disk", "#c_disk"), + } + SELECT_TO_FIELD = {sel[1:]: f for f, (sel, _i) in RES_FIELDS.items()} + INPUT_TO_FIELD = {inp[1:]: f for f, (_s, inp) in RES_FIELDS.items()} def entry_label(e): star = " *" if e.get("default") else "" @@ -330,6 +340,7 @@ def run_deploy_form(ctx, run_app: bool = True): .grouptitle { color: $accent; text-style: bold; padding: 1 0 0 0; } SelectionList { height: 10; border: solid $panel; } RadioSet { height: auto; layout: horizontal; } + .freeval { display: none; } #reslabel { color: $text-muted; } EditVMScreen { align: center middle; } #editbox { @@ -364,6 +375,7 @@ def run_deploy_form(ctx, run_app: bool = True): self.arch = ctx.get("native") or arches[0] self.profile = "1" self.custom = {} + self._free = {} self.overrides = {} self.vms = [] self.rows = [] @@ -385,27 +397,52 @@ def run_deploy_form(ctx, run_app: bool = True): for label in ("x1", "x2", "x3", "x4"): yield RadioButton(label, value=label == "x1") yield RadioButton(t("custom")) + # Chaque ressource offre ses suggestions plus « libre… », + # qui révèle une saisie. C'est l'équivalent TUI de la règle + # de la CLI : une lettre choisit une suggestion, un chiffre + # vaut pour lui-même. yield Select( - [(str(c), c) for c in ctx["cpu_presets"]], + [(str(c), c) for c in ctx["cpu_presets"]] + + [(t("free value…"), FREE)], prompt=t("vCPU"), id="f_vcpus", disabled=True, ) + yield Input( + placeholder=t("vCPU"), + id="c_vcpus", + classes="freeval", + disabled=True, + ) yield Select( [ (f"{m} ({m // 1024}G)", m) for m in ctx["ram_presets"] - ], + ] + + [(t("free value…"), FREE)], prompt=t("RAM (MB)"), id="f_ram", disabled=True, ) + yield Input( + placeholder=t("RAM (MB)"), + id="c_ram", + classes="freeval", + disabled=True, + ) yield Select( - [(d, d) for d in ctx["disk_presets"]], + [(d, d) for d in ctx["disk_presets"]] + + [(t("free value…"), FREE)], prompt=t("Disk"), id="f_disk", disabled=True, ) + yield Input( + placeholder=t("Disk (e.g. 250G, 1.5T)"), + id="c_disk", + classes="freeval", + disabled=True, + ) yield Static("ERPLibre", classes="grouptitle") yield Checkbox( t("Install ERPLibre"), @@ -588,19 +625,50 @@ def run_deploy_form(ctx, run_app: bool = True): index = event.radio_set.pressed_index self.profile = "custom" if index == 4 else str(index + 1) custom = self.profile == "custom" - for wid in ("#f_vcpus", "#f_ram", "#f_disk"): - self.query_one(wid, Select).disabled = not custom + for field, (sel, inp) in RES_FIELDS.items(): + self.query_one(sel, Select).disabled = not custom + free = custom and self._free.get(field) + self._show_free(field, free) self._recompute() + def _show_free(self, field, visible) -> None: + """Montre ou cache la saisie libre d'une ressource.""" + widget = self.query_one(RES_FIELDS[field][1], Input) + widget.display = bool(visible) + widget.disabled = not visible + def on_selection_list_selected_changed(self, event) -> None: self._recompute() def on_select_changed(self, event) -> None: - mapping = {"f_vcpus": "vcpus", "f_ram": "ram", "f_disk": "disk"} - field = mapping.get(event.select.id) + field = SELECT_TO_FIELD.get(event.select.id) + if not field: + return + if event.value is FREE: + # La valeur retenue est celle de la saisie, pas ce choix-ci. + self._free[field] = True + self._show_free(field, True) + self.query_one(RES_FIELDS[field][1], Input).focus() + self._apply_free(field) + elif event.value is not SELECT_NULL: + self._free[field] = False + self._show_free(field, False) + self.custom[field] = event.value + self._recompute() + + def _apply_free(self, field) -> None: + """Relit la saisie libre. Une valeur invalide n'écrase rien : le + profil retombe alors sur celle du catalogue.""" + raw = self.query_one(RES_FIELDS[field][1], Input).value.strip() + if field == "disk": + self.custom[field] = parse_disk(raw) or "" + else: + self.custom[field] = positive_int(raw, 0) + + def on_input_changed(self, event) -> None: + field = INPUT_TO_FIELD.get(event.input.id) if field: - if event.value is not SELECT_NULL: - self.custom[field] = event.value + self._apply_free(field) self._recompute() def on_checkbox_changed(self, event) -> None: diff --git a/script/todo/todo.py b/script/todo/todo.py index 0d9fa62..f55798b 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4507,7 +4507,7 @@ class TODO: # KVM autorise plus de vCPU que de cœurs (surengagement) : on prévient # plutôt que d'écrêter, contrairement au multiplicateur x1..x4 qui, lui, # est un calcul automatique et se borne aux cœurs de l'hôte. - _QEMU_CPU_PRESETS = (1, 2, 4, 6, 8, 16, 24, 32) + _QEMU_CPU_PRESETS = (1, 2, 3, 4, 6, 8, 16, 24, 32) @staticmethod def _plural(word, count): diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index cea0ecd..41dd0ab 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1973,6 +1973,14 @@ TRANSLATIONS = { "fr": "RAM (Mo)", "en": "RAM (MB)", }, + "free value…": { + "fr": "valeur libre…", + "en": "free value…", + }, + "Disk (e.g. 250G, 1.5T)": { + "fr": "Disque (ex. 250G, 1,5T)", + "en": "Disk (e.g. 250G, 1.5T)", + }, "all archs": { "fr": "toutes", "en": "all", From b6de9b5506a40242412d98cd47fa0dd8559c8f4e Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 07:52:52 -0400 Subject: [PATCH 05/41] [ADD] tui qemu: resume an installation already running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installs run detached (setsid -f): closing the terminal does not stop them, but it lost the only view onto them. With no way to resume, the only way out was deleting the VMs and starting over. Picking "Deploy" now looks at the latest run: if VMs there still lack an exit marker, it offers to reopen its monitoring instead of starting another. Time since the last write is shown, a dead run being otherwise indistinguishable from a live one. Only the latest run is examined: an old one left without a marker would flag a phantom install forever. Dry-run creates nothing, so it does not ask. "Reopen monitoring" moves to the Deployment section, where one looks for it. --- FR --- Les installs partent détachées (setsid -f) : fermer le terminal ne les arrête pas, mais faisait perdre la seule vue dessus. Sans moyen de reprendre, la seule issue était d'effacer les VM et de recommencer. Choisir « Déployer » regarde donc le dernier run : s'il lui reste des VM sans marqueur de sortie, il propose de rouvrir son suivi plutôt que d'en lancer un autre. Le silence depuis la dernière écriture est affiché, un run mort n'étant pas distinguable autrement d'un run vivant. Seul le dernier run est examiné : un run ancien laissé sans marqueur signalerait éternellement une install fantôme. L'aperçu ne crée rien, il ne pose pas la question. « Rouvrir le suivi » rejoint la section Déploiement, où on le cherche. Assisted-by: Claude Opus 5 --- script/todo/qemu_install_monitor.py | 40 +++++++++++++ script/todo/todo.py | 91 ++++++++++++++++++++++------- script/todo/todo_i18n.py | 24 ++++++++ 3 files changed, 134 insertions(+), 21 deletions(-) diff --git a/script/todo/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index 7b68617..d25ee20 100644 --- a/script/todo/qemu_install_monitor.py +++ b/script/todo/qemu_install_monitor.py @@ -261,6 +261,46 @@ def read_status(log_path: str) -> tuple[str, int | None]: return "running", None +def run_progress(run: dict) -> dict: + """Avancement d'un run : combien de VM tournent encore, et depuis quand + plus rien n'a été écrit. `idle` sert à distinguer une install vivante d'un + run laissé pour mort — le marqueur de sortie manque dans les deux cas.""" + active = final = 0 + latest = 0.0 + for vm in run.get("vms", []): + log = vm.get("log") or "" + state, _code = read_status(log) + if state in ("done", "failed"): + final += 1 + else: + active += 1 + try: + latest = max(latest, os.path.getmtime(log)) + except OSError: + pass + return { + "active": active, + "final": final, + "total": active + final, + "idle": (time.time() - latest) if latest else None, + } + + +def active_run(): + """Le run le PLUS RÉCENT s'il a encore des VM en cours, sinon None. + + Les installs tournent détachées (`setsid -f`) : fermer le terminal laisse + le travail se poursuivre mais fait perdre la seule vue dessus. On ne + regarde que le dernier run — un run ancien resté sans marqueur de sortie + signalerait éternellement une install fantôme.""" + runs = list_install_runs() + if not runs: + return None + run = dict(runs[0]) + run.update(run_progress(run)) + return run if run["total"] and run["active"] else None + + # Listes d'ignore reprises de script/test/run_parallel_test.py (erreurs/ # avertissements connus et bénins) : on réutilise la MÊME logique de détection # que la suite de tests ERPLibre pour analyser les logs d'installation. diff --git a/script/todo/todo.py b/script/todo/todo.py index f55798b..c4e7d7f 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1243,6 +1243,11 @@ class TODO: ) }, {"prompt_description": t("Download a cloud image only")}, + { + "prompt_description": t( + "Reopen install monitoring (last run / history)" + ) + }, {"section": t("Manage")}, {"prompt_description": t("List VMs (virsh list --all)")}, {"prompt_description": t("Show a VM IP address")}, @@ -1255,11 +1260,6 @@ class TODO: "Test a VM (open Odoo in a CLI browser)" ) }, - { - "prompt_description": t( - "Reopen install monitoring (last run / history)" - ) - }, {"prompt_description": t("Statistics (installs, durations, VMs)")}, { "prompt_description": t( @@ -1286,21 +1286,21 @@ class TODO: elif status == "3": self._qemu_download_image() elif status == "4": - self._qemu_list_vms(ask_advanced=True) - elif status == "5": - self._qemu_show_ip() - elif status == "6": - self._qemu_console() - elif status == "7": - self._qemu_resize_disk() - elif status == "8": - self._qemu_delete_vm() - elif status == "9": - self._qemu_cleanup() - elif status == "10": - self._qemu_test_vm() - elif status == "11": self._qemu_reopen_monitor() + elif status == "5": + self._qemu_list_vms(ask_advanced=True) + elif status == "6": + self._qemu_show_ip() + elif status == "7": + self._qemu_console() + elif status == "8": + self._qemu_resize_disk() + elif status == "9": + self._qemu_delete_vm() + elif status == "10": + self._qemu_cleanup() + elif status == "11": + self._qemu_test_vm() elif status == "12": self._qemu_stats() elif status == "13": @@ -2367,16 +2367,62 @@ class TODO: except ValueError: print(t("Invalid selection.")) return + self._qemu_open_monitor(run["manifest"]) + + def _qemu_open_monitor(self, manifest): + """Ouvre le dashboard sur un manifeste, en installant Textual au + besoin. Deux entrées y mènent — l'historique et la reprise proposée + avant un déploiement — d'où une seule définition.""" + from script.todo import qemu_install_monitor as mon + try: - mon.run_monitor(run["manifest"]) + mon.run_monitor(manifest) except ImportError: from script.todo import textual_setup if textual_setup.ensure(): - mon.run_monitor(run["manifest"]) + mon.run_monitor(manifest) except Exception as exc: print(f"{t('Command failed: ')}{exc}") + def _qemu_active_install(self): + """Propose de reprendre le suivi quand une installation tourne encore. + + Les installs partent détachées (`setsid -f`) : fermer le terminal ne + les arrête pas, mais faisait perdre la seule vue dessus, et la seule + issue connue était de tout effacer pour recommencer. + + True si l'on ne doit PAS enchaîner sur un déploiement.""" + try: + from script.todo import qemu_install_monitor as mon + + run = mon.active_run() + except Exception: + return False + if not run: + return False + names = ", ".join(v.get("name", "?") for v in run["vms"]) + print( + f"\n⏳ {t('An install is still running:')} {run['label']} — " + f"{run['active']}/{run['total']} {t('VM(s) in progress')}" + ) + print(f" {names}") + if run.get("idle") is not None: + # Un silence prolongé trahit un run mort dont le marqueur de sortie + # ne viendra jamais : l'utilisateur tranche mieux que nous. + print( + f" {t('Last activity:')} {mon._fmt_secs(int(run['idle']))}" + ) + print(f"\n [1] {t('Reopen that monitoring')} *") + print(f" [2] {t('Deploy anyway (new run)')}") + print(f" [0] {t('Back')}") + sel = input(t("Choice (number, blank = reopen): ")).strip() + if sel == "2": + return False + if sel != "0": + self._qemu_open_monitor(run["manifest"]) + return True + def _qemu_choose_cli_browser(self): """Offre la LISTE des navigateurs CLI installés, plus une option pour en INSTALLER un autre, et renvoie celui choisi, sinon None.""" @@ -5170,6 +5216,9 @@ class TODO: last = self._qemu_last_run_line() if last: print(last) + # Un aperçu ne crée rien : il n'a pas à interroger sur un run en cours. + if not dry_run and self._qemu_active_install(): + return self._qemu_check_libvirt_group() self._qemu_check_kvm() diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 41dd0ab..498841c 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1973,6 +1973,30 @@ TRANSLATIONS = { "fr": "RAM (Mo)", "en": "RAM (MB)", }, + "An install is still running:": { + "fr": "Une installation est encore en cours :", + "en": "An install is still running:", + }, + "VM(s) in progress": { + "fr": "VM en cours", + "en": "VM(s) in progress", + }, + "Last activity:": { + "fr": "Dernière activité il y a :", + "en": "Last activity:", + }, + "Reopen that monitoring": { + "fr": "Rouvrir ce suivi", + "en": "Reopen that monitoring", + }, + "Deploy anyway (new run)": { + "fr": "Déployer quand même (nouveau run)", + "en": "Deploy anyway (new run)", + }, + "Choice (number, blank = reopen): ": { + "fr": "Choix (numéro, vide = rouvrir) : ", + "en": "Choice (number, blank = reopen): ", + }, "free value…": { "fr": "valeur libre…", "en": "free value…", From 42391575d61adc9742a2372d6d9ba3e4e993a314 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 08:03:32 -0400 Subject: [PATCH 06/41] [REM] install: drop Ubuntu 20.04 and 22.04 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two blockers on 20.04, neither worth carrying. "TypeError: unsupported operand type(s) for |" fired before the install even started: update_env_version.py imports erplibre_state as the very first thing "make install_os" does, hence before pyenv, on the system Python 3.8, where a "str | None" annotation is evaluated at import. And cryptography now demands OpenSSL 3, while focal ships 1.1.1. Annotations are therefore deferred, a contract the bootstrap file already stated in a comment and now holds across script/version and script/install. But the releases themselves go: pikepdf requires qpdf >= 12.2, compiled in C++20, when focal ships GCC 9. --- FR --- Deux blocages sur 20.04, aucun ne méritant d'être porté. « TypeError: unsupported operand type(s) for | » se déclenchait avant même le début de l'installation : update_env_version.py importe erplibre_state en tout premier lieu de « make install_os », donc avant pyenv, sur le Python 3.8 du système, où une annotation « str | None » est évaluée à l'import. Et cryptography exige désormais OpenSSL 3, quand focal livre 1.1.1. Les annotations sont donc différées, contrat que le fichier d'amorçage énonçait déjà en commentaire et qui tient maintenant sur script/version et script/install. Mais les versions elles-mêmes partent : pikepdf réclame qpdf >= 12.2, compilé en C++20, quand focal livre GCC 9. Assisted-by: Claude Opus 5 --- script/install/install_debian_dependency.sh | 19 +++++++++++ script/install/install_locally.sh | 31 ++++++++++++++++- script/qemu/README.base.md | 15 ++++++++ script/qemu/README.fr.md | 8 +++++ script/qemu/README.md | 7 ++++ script/qemu/deploy_qemu.py | 38 +++++++++++++++++++++ script/todo/todo.py | 7 ++++ script/version/erplibre_state.py | 8 +++++ 8 files changed, 132 insertions(+), 1 deletion(-) diff --git a/script/install/install_debian_dependency.sh b/script/install/install_debian_dependency.sh index e0022ea..c7e6a6f 100755 --- a/script/install/install_debian_dependency.sh +++ b/script/install/install_debian_dependency.sh @@ -58,6 +58,25 @@ fi #-------------------------------------------------- if [ "$(uname -m)" = "s390x" ]; then echo "Arch s390x detected" + # Sur s390x, aucune roue PyPI : tout se compile contre les bibliothèques de + # la distribution. Deux versions n'en ont pas d'assez récentes. + # + # 20.04 : pikepdf réclame qpdf >= 12.2, dont la compilation exige C++20. + # focal livre GCC 9 et ne publie PAS g++-10 pour s390x — vérifié. + # La compilation s'arrête sur « fatal error: concepts: No such file + # or directory », sans contournement possible. + # 22.04 : même mur qpdf, franchissable en théorie, abandonné avec la 20.04 + # plutôt que de maintenir un chemin de plus. + # + # On refuse ICI, avant une heure de compilation émulée pour rien. + case "${UBUNTU_VERSION}" in + 20.04 | 22.04) + echo "Ubuntu ${UBUNTU_VERSION} n'est plus supporte sur s390x :" + echo " sa chaine d'outils est trop ancienne pour ERPLibre." + echo " Utilisez Ubuntu 24.04 ou plus recent." + exit 1 + ;; + esac # Sur une VM fraîche, l'index apt peut être vide : « apt install » échouerait # alors sur TOUT le lot, et l'absence d'un seul paquet ne se voit que bien # plus loin, sous la forme d'une commande introuvable. diff --git a/script/install/install_locally.sh b/script/install/install_locally.sh index 350b86a..585fe26 100755 --- a/script/install/install_locally.sh +++ b/script/install/install_locally.sh @@ -81,10 +81,39 @@ if [[ "${EL_PHASE}" != "setup" ]]; then exit 1 fi + # s390x UNIQUEMENT — la borne ci-dessous ne doit toucher aucune autre + # architecture, et le test d'architecture vient donc en premier. + # + # Poetry tire keyring, donc SecretStorage, donc cryptography, dont la + # DERNIÈRE version : rien ne la borne à cette étape. Partout ailleurs c'est + # sans conséquence, pip y pose une roue manylinux qui embarque son propre + # OpenSSL en statique — vérifié, la 50.0.0 en publie pour x86_64, aarch64, + # ppc64le et armv7l. s390x est la SEULE sans roue : elle compile, contre + # l'OpenSSL du système. + # + # Or cryptography 47 a retiré le support d'OpenSSL 1.1.1 — vérifié version + # par version, les gardes « CRYPTOGRAPHY_OPENSSL_300_OR_GREATER » de + # fips.rs disparaissent entre la 46 et la 47 — et Ubuntu 20.04 livre + # 1.1.1f. D'où « EVP_default_properties_is_fips_enabled not found in ffi ». + # Sur s390x en 22.04 et au-delà, OpenSSL est en 3.x : aucune borne. + PIP_CONSTRAINT_CRYPTO="" + if [[ "$(uname -m)" == "s390x" ]]; then + OPENSSL_VER="$(pkg-config --modversion openssl 2>/dev/null \ + || openssl version 2>/dev/null | awk '{print $2}')" + case "${OPENSSL_VER}" in + 3.* | 4.*) ;; + "") echo "s390x : version d'OpenSSL indeterminee, aucune borne." ;; + *) + echo "s390x, OpenSSL ${OPENSSL_VER} < 3 : cryptography borne a <47 pour poetry." + PIP_CONSTRAINT_CRYPTO="cryptography<47" + ;; + esac + fi + # Delete artifacts created by pip, cause error in next "poetry install" if [[ ! -f "${POETRY_ODOO_PATH}" ]]; then echo -e "Install Poetry ${POETRY_ODOO_PATH}" - pip install poetry==${EL_POETRY_VERSION} + pip install ${PIP_CONSTRAINT_CRYPTO} poetry==${EL_POETRY_VERSION} poetry --version # Fix broken poetry by installing ignored dependence # poetry lock --no-update diff --git a/script/qemu/README.base.md b/script/qemu/README.base.md index 116a496..33daefb 100644 --- a/script/qemu/README.base.md +++ b/script/qemu/README.base.md @@ -177,6 +177,13 @@ Supported Ubuntu versions: `20.04`, `22.04`, `24.04` (default), `24.10`, `25.04`, `25.10`. Provide an explicit image path as a positional argument to override the automatic download location. +On **s390x**, `20.04` and `22.04` are **not supported**: no PyPI wheel exists +for that architecture, so everything is built against the distribution's +libraries, and pikepdf needs qpdf 12.2, whose build requires C++20. Focal +ships GCC 9 and publishes no `g++-10` for s390x — there is no way around it. +Use `24.04` or later. Both are still supported on amd64 and arm64, where pip +installs prebuilt wheels. + ## After deployment @@ -184,6 +191,14 @@ Versions Ubuntu supportées : `20.04`, `22.04`, `24.04` (défaut), `24.10`, `25.04`, `25.10`. Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. +Sur **s390x**, les `20.04` et `22.04` ne sont **pas supportées** : aucune roue +PyPI n'existe pour cette architecture, tout se compile donc contre les +bibliothèques de la distribution, et pikepdf réclame qpdf 12.2, dont la +compilation exige C++20. Focal livre GCC 9 et ne publie pas de `g++-10` pour +s390x — il n'y a pas de contournement. Utilisez la `24.04` ou plus récente. +Les deux restent supportées sur amd64 et arm64, où pip pose des roues +précompilées. + ## Après le déploiement diff --git a/script/qemu/README.fr.md b/script/qemu/README.fr.md index afae765..21dfe7f 100644 --- a/script/qemu/README.fr.md +++ b/script/qemu/README.fr.md @@ -96,6 +96,14 @@ Versions Ubuntu supportées : `20.04`, `22.04`, `24.04` (défaut), `24.10`, `25.04`, `25.10`. Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. +Sur **s390x**, les `20.04` et `22.04` ne sont **pas supportées** : aucune roue +PyPI n'existe pour cette architecture, tout se compile donc contre les +bibliothèques de la distribution, et pikepdf réclame qpdf 12.2, dont la +compilation exige C++20. Focal livre GCC 9 et ne publie pas de `g++-10` pour +s390x — il n'y a pas de contournement. Utilisez la `24.04` ou plus récente. +Les deux restent supportées sur amd64 et arm64, où pip pose des roues +précompilées. + ## Après le déploiement ```bash diff --git a/script/qemu/README.md b/script/qemu/README.md index f4d8b02..9335a43 100644 --- a/script/qemu/README.md +++ b/script/qemu/README.md @@ -90,6 +90,13 @@ Supported Ubuntu versions: `20.04`, `22.04`, `24.04` (default), `24.10`, `25.04`, `25.10`. Provide an explicit image path as a positional argument to override the automatic download location. +On **s390x**, `20.04` and `22.04` are **not supported**: no PyPI wheel exists +for that architecture, so everything is built against the distribution's +libraries, and pikepdf needs qpdf 12.2, whose build requires C++20. Focal +ships GCC 9 and publishes no `g++-10` for s390x — there is no way around it. +Use `24.04` or later. Both are still supported on amd64 and arm64, where pip +installs prebuilt wheels. + ## After deployment ```bash diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index f8f570b..f7e0f19 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -128,6 +128,31 @@ NON_X86_ARCHES: tuple[str, ...] = ("arm64", "s390x") # aarch64 sur geo.mirror.pkgbuild.com). S390X_DISTROS: tuple[str, ...] = ("ubuntu",) ARM64_DISTROS: tuple[str, ...] = ("ubuntu", "debian", "fedora") + +# Versions ABANDONNÉES sur une architecture donnée : publier une image cloud ne +# suffit pas, encore faut-il que la chaîne d'outils permette d'installer +# ERPLibre. Sur s390x, aucune roue PyPI n'existe : tout se compile contre les +# bibliothèques de la distribution, et deux versions n'en ont pas d'assez +# récentes. +# +# 20.04 : pikepdf réclame qpdf >= 12.2, dont la compilation exige C++20 — +# or focal livre GCC 9 et NE PUBLIE PAS g++-10 pour s390x. Sans +# issue. S'y ajoutaient Python 3.8, node 10, cargo 0.67 et +# OpenSSL 1.1.1, tous contournés, mais celui-là ne l'est pas. +# 22.04 : même mur qpdf, franchissable en théorie (GCC 11), abandonné avec +# la 20.04 pour ne pas maintenir un chemin de plus. +# +# amd64 et arm64 ne sont PAS concernés : pip y pose des roues précompilées. +UNSUPPORTED_VERSIONS: dict[str, dict[str, tuple[str, ...]]] = { + "s390x": {"ubuntu": ("20.04", "22.04")}, +} + + +def is_supported(distro: str, version: str, arch: str) -> bool: + """Faux si ce triplet est explicitement abandonné.""" + return version not in UNSUPPORTED_VERSIONS.get(arch, {}).get(distro, ()) + + # arch générique -> distros la publiant (pour valider --arch tôt). ARCH_DISTRO_SUPPORT: dict[str, tuple[str, ...]] = { "s390x": S390X_DISTROS, @@ -2084,6 +2109,19 @@ def main() -> None: f"Architecture {args.arch} indisponible pour {args.distro!r} : " f"images cloud publiées seulement pour {', '.join(supported)}." ) + # L'image cloud existe, mais la chaîne d'outils de cette version ne permet + # pas d'installer ERPLibre sur cette architecture. Mieux vaut le dire ici + # qu'après une heure de compilation émulée. + if not is_supported(args.distro, args.version, args.arch): + dropped = UNSUPPORTED_VERSIONS[args.arch][args.distro] + keep = [v for v in versions if v not in dropped] + sys.exit( + f"{args.distro} {args.version} n'est plus supporté sur " + f"{args.arch} : sa chaîne d'outils est trop ancienne pour " + f"ERPLibre (GCC 9 sur 20.04 ne compile pas le qpdf que réclame " + f"pikepdf, et aucun g++ plus récent n'y est publié).\n" + f" Versions supportées sur {args.arch} : {', '.join(keep)}." + ) code, default_osinfo, min_ram, min_disk = versions[args.version] if args.codename: code = args.codename diff --git a/script/todo/todo.py b/script/todo/todo.py index c4e7d7f..5a9b238 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4987,10 +4987,17 @@ class TODO: déployable, aussi bien pour la liste granulaire de la CLI que pour la liste à cocher du formulaire TUI.""" flat = [] + # Le catalogue ne propose que ce qui s'installe. La liste des versions + # abandonnées vit dans deploy_qemu.py, qui refuse aussi de les + # déployer : une seule source, donc pas d'écran offrant un choix que + # la couche du dessous rejettera. + supported = getattr(mod, "is_supported", None) for d in distros: versions_map, default_v = mod.DISTROS[d] for v, (_c, _o, ram, disk) in versions_map.items(): for a in self._qemu_arches_for(d, arch): + if supported and not supported(d, v, a): + continue flat.append( { "distro": d, diff --git a/script/version/erplibre_state.py b/script/version/erplibre_state.py index 790434c..9c5c529 100644 --- a/script/version/erplibre_state.py +++ b/script/version/erplibre_state.py @@ -9,8 +9,16 @@ This file tracks what is installed in the current ERPLibre workspace: - Which Odoo versions are installed, and with which options (extra, etc.) - Whether the mobile project is active - The currently active Odoo version + +`update_env_version.py` imports this module as the FIRST thing `make +install_os` runs, donc avant pyenv : il s'exécute sur le Python DU SYSTÈME, +3.8 sur Ubuntu 20.04. Les annotations doivent rester différées — sans quoi +« str | None » (PEP 604) ou « list[str] » (PEP 585) sont évaluées à +l'import et lèvent un TypeError avant même que l'installation commence. """ +from __future__ import annotations + import json import logging import os From 571ccf3c90622b466018d572358cd8e0d4e5b2f3 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 08:36:13 -0400 Subject: [PATCH 07/41] [FIX] qemu monitor: follow the DHCP lease, and keep the log talking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Monitoring stayed on the same address for 1170 s, never catching up with the VM. Two holes, in the very re-resolution meant to prevent that. The lease fallback required an answer on port 22. But dnsmasq keeps one lease per MAC: when cloud-init sets the real hostname and the DHCP client asks again, the lease MOVES the address. The old one no longer belongs to the VM and sshd will never answer there. The lease therefore wins as soon as it stops listing the current address. The other hole explains the silence: virsh was muted on both branches, so an unreachable libvirt kept the initial IP without a single line saying so. The log went quiet for a quarter of an hour for the same reason -- cloud-init holds the package lock while writing nothing. --- FR --- Le suivi restait sur la même adresse pendant 1170 s, sans jamais rattraper la VM. Deux trous, dans la re-résolution censée l'éviter. Le repli par bail exigeait une réponse sur le port 22. Or dnsmasq garde un bail par MAC : quand cloud-init pose le vrai nom d'hôte et que le client DHCP redemande, le bail DÉPLACE l'adresse. L'ancienne n'appartient plus à la VM et sshd n'y répondra jamais. Le bail l'emporte donc dès qu'il cesse de lister l'adresse courante. L'autre trou explique le silence : virsh était muet sur les deux branches, si bien qu'un libvirt injoignable conservait l'IP initiale sans une ligne pour le dire. Le log se taisait un quart d'heure pour la même raison — cloud-init tient le verrou des paquets sans rien écrire. Assisted-by: Claude Opus 5 --- script/todo/qemu_install_monitor.py | 45 ++++++++++++++++++++++++++--- script/todo/todo.py | 14 ++++++++- script/todo/todo_i18n.py | 26 +++++++++++++++++ 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/script/todo/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index d25ee20..2558bf0 100644 --- a/script/todo/qemu_install_monitor.py +++ b/script/todo/qemu_install_monitor.py @@ -98,6 +98,15 @@ def _launch_one( msg_wait = t("Waiting for the VM to start (boot + cloud-init)") msg_slow = t("(an emulated architecture can be slow; this is normal)") msg_ready = t("VM ready - starting the ERPLibre install") + msg_giveup = t( + "cloud-init still running after 20 min - install starts anyway" + " (it waits for cloud-init first)" + ) + msg_novirsh = t( + "WARNING libvirt unreachable: the IP will not be refreshed" + " (libvirt group? re-login required)" + ) + msg_moved = t("DHCP lease moved:") # L'IP est RÉSOLUE À CHAQUE TOUR, jamais figée. Au 1er boot la VM prend un # bail sous le nom par défaut de l'image, puis cloud-init pose le vrai nom # d'hôte et le client DHCP en redemande un AUTRE. L'adresse connue au @@ -130,17 +139,38 @@ def _launch_one( 'vsh() { virsh --connect qemu:///system "$@" 2>/dev/null ' '|| sudo -n virsh --connect qemu:///system "$@" 2>/dev/null; }; ' ) + # Deux trous rendaient cette ré-résolution incapable de rattraper une IP + # qui bouge — le cas exact où elle sert : + # + # - « vsh » est muet des deux côtés. Quand libvirt est injoignable (hors du + # groupe libvirt, et « sudo -n » refusé faute de tty dans ce processus + # détaché), la ré-résolution ne renvoie JAMAIS rien : l'IP de départ est + # gardée jusqu'au bout sans qu'une seule ligne du log ne le dise. + # - le repli sur les baux exigeait une réponse sur le port 22. Or dnsmasq + # ne garde qu'un bail par MAC : quand cloud-init pose le vrai nom d'hôte + # et que le client DHCP redemande, le bail DÉPLACE l'adresse. L'ancienne + # n'appartient plus à cette VM, mais on l'attendait quand même — et sshd + # n'y répondra jamais. refresh = ( ( + f"raw=$(vsh domifaddr {name_q} --source lease); vrc=$?; " + 'if [ $vrc -ne 0 ] && [ -z "$vwarn" ]; then vwarn=1; ' + f"echo {shlex.quote(' ' + msg_novirsh)} >> {log_q}; fi; " + "cands=$(echo \"$raw\" | grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' " + "| grep -v '^127\\.'); " f"n=$(vsh domifaddr {name_q} --source agent " "| grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' " "| grep -v '^127\\.' | head -1); " 'if [ -z "$n" ]; then ' - f"for c in $(vsh domifaddr {name_q} --source lease " - "| grep -oE '([0-9]{1,3}\\.){3}[0-9]{1,3}' " - "| grep -v '^127\\.'); do " + "for c in $cands; do " 'timeout 2 bash -c "echo > /dev/tcp/$c/22" 2>/dev/null ' '&& n="$c"; done; fi; ' + # Le bail ne mentionne plus l'adresse courante : elle est périmée, + # on suit le bail sans attendre que sshd réponde. + 'if [ -z "$n" ] && [ -n "$cands" ] && ' + '! echo "$cands" | grep -Fqx "$ip"; then ' + 'n=$(echo "$cands" | tail -1); ' + f'echo " {msg_moved} $ip -> $n" >> {log_q}; fi; ' '[ -n "$n" ] && ip="$n"; ' ) if name @@ -151,17 +181,24 @@ def _launch_one( f"{vsh if name else ''}" f"echo {shlex.quote('== ' + msg_wait + ' ==')} >> {log_q}; " f"echo {shlex.quote(' ' + msg_slow)} >> {log_q}; " + f"seen=0; " f"for i in $(seq 1 240); do " f"{refresh}" f'st=$(ssh {SSH_OPTS} -o BatchMode=yes "erplibre@$ip" ' f"{shlex.quote(ci_probe)} 2>/dev/null); " f'case "$st" in ' - f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) break;; " + f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) seen=1; break;; " f"esac; " f"if [ $((i % 6)) -eq 0 ]; then " f'echo " ... $((i*5))s ($ip)" >> {log_q}; fi; ' f"sleep 5; done; " + # La boucle peut s'ÉPUISER au lieu de rompre : sous émulation, + # cloud-init dépasse volontiers 20 min. Annoncer « VM prête » dans les + # deux cas donnait un message faux juste avant le plus long silence du + # log — c'est l'installation qui attend alors la fin de cloud-init. + f'if [ "$seen" = 1 ]; then ' f"echo {shlex.quote('== ' + msg_ready + ' ==')} >> {log_q}; " + f"else echo {shlex.quote('== ' + msg_giveup + ' ==')} >> {log_q}; fi; " f'echo " → $ip" >> {log_q}; ' f'ssh {SSH_OPTS} "erplibre@$ip" {shlex.quote(remote_cmd)} ' f">> {log_q} 2>&1; " diff --git a/script/todo/todo.py b/script/todo/todo.py index 5a9b238..6b83475 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4341,9 +4341,21 @@ class TODO: # tient le verrou apt/dnf/pacman -> sinon « unable to lock # database » (Arch) / « Could not get lock » (apt). timeout pour ne # pas bloquer indéfiniment si cloud-init traîne. - "command -v cloud-init >/dev/null 2>&1 && " + # + # Cette attente dure jusqu'à 15 min et n'écrivait RIEN : sur une + # architecture émulée, le log restait muet d'un quart d'heure juste + # après avoir annoncé le début de l'installation, ce qui se lit + # comme un blocage. On encadre donc l'attente de deux lignes, et le + # « status » final dit si elle a abouti ou expiré. + "if command -v cloud-init >/dev/null 2>&1; then " + + 'echo "== ' + + t("Waiting for cloud-init to finish (up to 15 min)") + + ' =="; ' "sudo timeout 900 cloud-init status --wait >/dev/null 2>&1 " "|| true; " + + f'echo " {t("cloud-init:")} $(cloud-init status 2>/dev/null ' + '| head -1)"; ' + "fi; " # Coupé AVANT les apt-get ci-dessous : sinon apt-daily peut reprendre # le verrou entre l'attente cloud-init et l'installation. + no_auto_upgrade + diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 498841c..5fc4a74 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1973,6 +1973,32 @@ TRANSLATIONS = { "fr": "RAM (Mo)", "en": "RAM (MB)", }, + "cloud-init still running after 20 min - install starts anyway" + " (it waits for cloud-init first)": { + "fr": "cloud-init tourne encore apres 20 min - l'installation demarre" + " quand meme (elle attend d'abord cloud-init)", + "en": "cloud-init still running after 20 min - install starts anyway" + " (it waits for cloud-init first)", + }, + "Waiting for cloud-init to finish (up to 15 min)": { + "fr": "Attente de la fin de cloud-init (jusqu'a 15 min)", + "en": "Waiting for cloud-init to finish (up to 15 min)", + }, + "cloud-init:": { + "fr": "cloud-init :", + "en": "cloud-init:", + }, + "WARNING libvirt unreachable: the IP will not be refreshed" + " (libvirt group? re-login required)": { + "fr": "ATTENTION libvirt injoignable : l'IP ne sera pas rafraichie" + " (groupe libvirt ? reconnexion necessaire)", + "en": "WARNING libvirt unreachable: the IP will not be refreshed" + " (libvirt group? re-login required)", + }, + "DHCP lease moved:": { + "fr": "bail DHCP deplace :", + "en": "DHCP lease moved:", + }, "An install is still running:": { "fr": "Une installation est encore en cours :", "en": "An install is still running:", From c7b9d2396db929c972043a32c5f5107b7b8c8e47 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 08:59:10 -0400 Subject: [PATCH 08/41] [ADD] tui qemu: one run per installation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The form's parallelism was pinned at 4, while the CLI already counted host cores: two interfaces, two answers. A box ticked by default now gives one run per install -- five VMs, five deployments -- and the core count stops capping it. Unticking hands control back to the dropdown, whose default finally follows the host. The CLI offers the same toggle through "n". --- FR --- Le parallélisme du formulaire était figé à 4, quand la CLI comptait déjà les cœurs de l'hôte : deux interfaces, deux réponses. Une case cochée par défaut donne désormais une exécution par installation — cinq VM, cinq déploiements — et le nombre de cœurs cesse alors de plafonner. La décocher rend la main à la liste, dont le défaut suit enfin l'hôte. La CLI offre la même bascule par « n ». Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 23 +++++++++++++++++++++-- script/todo/todo.py | 33 ++++++++++++++++++++++----------- script/todo/todo_i18n.py | 8 ++++++++ 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index c49511a..247891a 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -491,10 +491,20 @@ def run_deploy_form(ctx, run_app: bool = True): id="f_sshcfg", ) yield Static(t("Parallelism"), classes="grouptitle") + # Cochée, la case donne une exécution PAR installation : + # le plafond du nombre de CPU ne s'applique plus. Décochée, + # le nombre reprend la main, et son défaut suit l'hôte — + # la CLI comptait déjà les CPU, la TUI restait figée à 4. + yield Checkbox( + t("One run per install"), + value=defaults.get("par_per_install", True), + id="f_par_all", + ) yield Select( [(str(n), n) for n in range(1, host_cpu + 1)], - value=min(4, host_cpu), + value=host_cpu, allow_blank=False, + disabled=True, id="f_par", ) with Vertical(id="right"): @@ -674,6 +684,8 @@ def run_deploy_form(ctx, run_app: bool = True): def on_checkbox_changed(self, event) -> None: if event.checkbox.id == "f_install": self._recompute() # le disque annoncé inclut le +5 G ERPLibre + elif event.checkbox.id == "f_par_all": + self.query_one("#f_par", Select).disabled = event.value # -- actions ---------------------------------------------------- # def action_select_all(self) -> None: @@ -732,7 +744,14 @@ def run_deploy_form(ctx, run_app: bool = True): or "", "install": install, "add_ssh_config": self.query_one("#f_sshcfg", Checkbox).value, - "parallelism": self.query_one("#f_par", Select).value, + # Une exécution par installation : le nombre de VM retenues + # fait foi. Le déploiement le borne ensuite à ce même nombre, + # donc une valeur haute ne crée jamais de travailleur inutile. + "parallelism": ( + max(1, len(self.vms)) + if self.query_one("#f_par_all", Checkbox).value + else self.query_one("#f_par", Select).value + ), } def action_preview(self) -> None: diff --git a/script/todo/todo.py b/script/todo/todo.py index 6b83475..12babca 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -5581,17 +5581,28 @@ class TODO: print(t("Nothing to create - every VM already exists.")) return None - # Nombre de déploiements en parallèle. Défaut = nombre de CPU de l'hôte - # (borné par le nombre de VM). On affiche aussi le NB DE VM à déployer. - default_par = min(n_jobs, os.cpu_count() or 4) or 1 - raw = input( - f"{t('Parallel deployments (default:')} {default_par}, " - f"{n_jobs} {t('VMs')}): " - ).strip() - try: - parallelism = max(1, int(raw)) if raw else default_par - except ValueError: - parallelism = default_par + # Nombre de déploiements en parallèle. Par défaut UNE EXÉCUTION PAR + # INSTALLATION : le plafond du nombre de CPU ne s'applique pas, c'est + # le nombre de VM qui fait foi. « n » retombe sur ce plafond, et un + # chiffre vaut pour lui-même — même règle que les autres invites. + default_par = n_jobs or 1 + cpu_par = min(n_jobs, os.cpu_count() or 4) or 1 + print(f" [n] {t('limit to host cores')} ({cpu_par})") + raw = ( + input( + f"{t('Parallel deployments (default:')} {default_par}, " + f"{n_jobs} {t('VMs')}): " + ) + .strip() + .lower() + ) + if raw == "n": + parallelism = cpu_par + else: + try: + parallelism = max(1, int(raw)) if raw else default_par + except ValueError: + parallelism = default_par spec = { "res_label": res_label, diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 5fc4a74..afc0cd2 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2023,6 +2023,14 @@ TRANSLATIONS = { "fr": "Choix (numéro, vide = rouvrir) : ", "en": "Choice (number, blank = reopen): ", }, + "One run per install": { + "fr": "une execution par installation", + "en": "One run per install", + }, + "limit to host cores": { + "fr": "limiter au nombre de coeurs de l'hote", + "en": "limit to host cores", + }, "free value…": { "fr": "valeur libre…", "en": "free value…", From 891d412aa2f9f9aed087f464393b3e32413c07c9 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 09:24:59 -0400 Subject: [PATCH 09/41] [UPD] tui qemu: pick the VM by number for its state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changing state meant retyping the name, on a single comma-separated line. Names are long and alike -- two of them sometimes differ by a suffix only -- and a typo here decided whether a machine started or shut down. The screen now reuses the numbered list from the delete screen, "all" included, and its parser, which already accepts names: nothing is lost for those who typed them. The warning on an unknown entry is kept, that parser otherwise dropping it without a word. --- FR --- Changer l'état demandait de retaper le nom, sur une seule ligne séparée par des virgules. Les noms sont longs et se ressemblent — deux d'entre eux ne diffèrent parfois que par un suffixe — et une faute de frappe portait ici sur le démarrage ou l'extinction d'une machine. L'écran reprend donc la liste numérotée de la suppression, « all » compris, et son analyseur, qui accepte déjà les noms : rien n'est perdu pour qui les tapait. L'avertissement sur une entrée inconnue est conservé, cet analyseur l'écartant autrement sans un mot. Assisted-by: Claude Opus 5 --- script/todo/todo.py | 46 +++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/script/todo/todo.py b/script/todo/todo.py index 12babca..45f5984 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -2137,20 +2137,34 @@ class TODO: if not names: print(f"\n{t('No VM found.')}") return - print(f"\n{t('Available VMs:')} {', '.join(names)}") - raw = input(t("VMs to change (comma-separated): ")).strip() - targets = [n.strip() for n in raw.split(",") if n.strip()] - if not targets: + # Liste NUMÉROTÉE, comme l'écran de suppression : les noms de VM sont + # longs et se ressemblent, les retaper invite à la faute de frappe sur + # une commande qui change l'état d'une machine. + print(f"\n{t('Available VMs:')}") + for i, n in enumerate(names, 1): + print(f" [{i}] {n}") + print(f" [all] {t('select all')}") + raw = input(t("Selection (numbers, or 'all'): ")).strip() + if not raw: print(t("Nothing selected.")) return - # Résoudre les ID -> noms et valider l'existence. - resolved, unknown = [], [] - known = set(names) - for tgt in targets: - real = self._qemu_domname(tgt) - (resolved if real in known else unknown).append(real) - if unknown: - print(f"{t('Unknown VM(s):')} {', '.join(unknown)}") + if raw.lower() in ("all", "*"): + resolved = list(names) + else: + resolved = self._parse_index_selection(raw.lower(), names) + # Le parseur ignore en silence ce qu'il ne reconnaît pas. Sur une + # sélection qui va démarrer ou éteindre des VM, un numéro hors + # liste doit être dit, pas escamoté. + unknown = [ + tok + for tok in re.split(r"[\s,]+", raw.strip()) + if tok and tok not in names and not self._is_index(tok, names) + ] + if unknown: + print(f"{t('Unknown VM(s):')} {', '.join(unknown)}") + return + if not resolved: + print(t("Nothing selected.")) return # Choix de l'état cible : ouvrir (démarrer) ou fermer (éteindre). print(f"\n{t('Target state:')}") @@ -3812,6 +3826,14 @@ class TODO: pass return 0 + @staticmethod + def _is_index(token, options): + """Vrai si le jeton est un numéro valide dans la liste (1-based).""" + try: + return 1 <= int(token) <= len(options) + except ValueError: + return False + @staticmethod def _parse_index_selection(raw, options): """« 1 3 » ou « 1,3 » -> sous-liste d'options (indices 1-based).""" From bace74b32b14161b760180118493aea7fe351fed Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 09:49:17 -0400 Subject: [PATCH 10/41] [ADD] tui qemu: graphical VMs, with a GNOME desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VMs could only be servers. A server-or-graphical choice joins both interfaces, installing GNOME along with remote access to it. Packages come from the remote command, not cloud-init: their 1 to 2 GB would stretch an already long boot there while leaving no trace in the monitoring, and group installs do not go through it. The desktop therefore does not depend on ERPLibre -- a VM may be wanted graphical and bare. Each distribution has its own names, taken from the source: Arch has no xrdp in its official repositories and takes TigerVNC. The SPICE display is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but nothing guarantees its kernel's DRM driver, whereas remote desktop works everywhere. --- FR --- Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique s'ajoute aux deux interfaces, et pose GNOME avec son accès distant. Les paquets viennent de la commande distante, pas de cloud-init : leurs 1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace dans le suivi, et les installations par groupe n'y passent pas. Le bureau ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue. Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui, marche partout. Assisted-by: Claude Opus 5 --- script/qemu/deploy_qemu.py | 26 ++- script/todo/qemu_deploy_form.py | 37 +++- script/todo/todo.py | 287 +++++++++++++++++++++++++------- script/todo/todo_i18n.py | 24 +++ 4 files changed, 301 insertions(+), 73 deletions(-) diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index f7e0f19..15f7d0d 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -1631,6 +1631,22 @@ def virt_install( # s390x n'a pas de port série ISA : la console est SCLP (ttysclp0), et non # ttyS0. Ailleurs (x86/arm64), console série classique. console_target = "sclp" if args.arch == "s390x" else "serial" + # Écran virtuel pour une VM graphique. s390x en est écarté : QEMU y expose + # bien « virtio-gpu-ccw », mais rien ne garantit que le noyau s390x de la + # distribution embarque le pilote DRM virtio-gpu — la VM démarrerait alors + # sur un écran noir. Le bureau distant, lui, marche partout : c'est la voie + # retenue pour cette architecture, et le message le dit. + graphics = args.graphics + video = [] + if args.desktop and graphics == "none": + if args.arch == "s390x": + print( + "\n s390x : pas d'écran virtuel (pilote DRM non garanti)." + "\n Le bureau sera accessible à distance, par le réseau." + ) + else: + graphics = "spice,listen=none" + video = ["--video", "virtio"] cmd = [ "virt-install", # Sans --connect, un utilisateur non root vise qemu:///session : le @@ -1660,7 +1676,7 @@ def virt_install( "--network", args.network, "--graphics", - args.graphics, + graphics, "--console", f"pty,target_type={console_target}", # Canal virtio de l'agent invité (org.qemu.guest_agent.0) : permet à @@ -1669,6 +1685,7 @@ def virt_install( "--channel", "unix,target.type=virtio,target.name=org.qemu.guest_agent.0", ] + cmd += video if args.arch == "s390x": # s390x (IBM Z) : machine s390-ccw-virtio, amorçage IPL/zipl depuis le # disque (ni BIOS ni UEFI/OVMF -> aucun --boot). @@ -1883,6 +1900,13 @@ def build_parser() -> argparse.ArgumentParser: default="none", help="Argument --graphics (défaut : none).", ) + g_vm.add_argument( + "--desktop", + action="store_true", + help="VM graphique : écran virtuel SPICE là où l'architecture le " + "permet. Les paquets GNOME sont posés par la commande d'installation, " + "pas ici.", + ) g_vm.add_argument( "--osinfo", help="Force la valeur --osinfo (sinon déduite)." ) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 247891a..264095e 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -177,6 +177,7 @@ def build_spec(vms, domains, form): "existing": [vm["name"] for vm in vms if vm["name"] in known], "ssh_key": form["ssh_key"], "timezone": form.get("timezone", ""), + "desktop": form.get("desktop", False), "install": form["install"], "add_ssh_config": form["add_ssh_config"], "parallelism": form["parallelism"], @@ -241,6 +242,7 @@ def run_deploy_form(ctx, run_app: bool = True): free_ram = ctx.get("free_ram") or 0 base_vcpus = ctx.get("base_vcpus") or 2 extra_disk = ctx.get("extra_disk_gb") or 0 + desktop_disk = ctx.get("desktop_disk_gb") or 0 defaults = ctx.get("defaults") or {} result = {"spec": None} @@ -443,6 +445,19 @@ def run_deploy_form(ctx, run_app: bool = True): classes="freeval", disabled=True, ) + # Serveur par défaut : c'est ce que sert une image cloud, + # et GNOME ajoute une à deux heures sur une architecture + # émulée. Le plan annonce le surcoût disque. + yield Static(t("VM type:"), classes="grouptitle") + with RadioSet(id="f_type"): + yield RadioButton( + t("Server (no graphical interface)"), + value=not defaults.get("desktop", False), + ) + yield RadioButton( + t("Graphical (server + GNOME desktop)"), + value=defaults.get("desktop", False), + ) yield Static("ERPLibre", classes="grouptitle") yield Checkbox( t("Install ERPLibre"), @@ -574,17 +589,18 @@ def run_deploy_form(ctx, run_app: bool = True): self.custom, self.overrides, ) - self.rows = plan_rows( - self.vms, - domains, - ( - extra_disk - if self.query_one("#f_install", Checkbox).value - else 0 - ), - ) + # ERPLibre et GNOME pèsent chacun sur le disque, et se cumulent. + grow = 0 + if self.query_one("#f_install", Checkbox).value: + grow += extra_disk + if self._desktop(): + grow += desktop_disk + self.rows = plan_rows(self.vms, domains, grow) self._render_plan() + def _desktop(self): + return self.query_one("#f_type", RadioSet).pressed_index == 1 + def _render_plan(self): table = self.query_one("#plan", DataTable) table.clear() @@ -631,6 +647,8 @@ def run_deploy_form(ctx, run_app: bool = True): if event.radio_set.id == "f_arch": self.arch = arches[event.radio_set.pressed_index] self._reload_catalog() + elif event.radio_set.id == "f_type": + self._recompute() # le disque annonce inclut GNOME elif event.radio_set.id == "f_profile": index = event.radio_set.pressed_index self.profile = "custom" if index == 4 else str(index + 1) @@ -742,6 +760,7 @@ def run_deploy_form(ctx, run_app: bool = True): "timezone": self.query_one("#f_tz", Input).value.strip() or ctx.get("timezone") or "", + "desktop": self._desktop(), "install": install, "add_ssh_config": self.query_one("#f_sshcfg", Checkbox).value, # Une exécution par installation : le nombre de VM retenues diff --git a/script/todo/todo.py b/script/todo/todo.py index 45f5984..d806595 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4303,11 +4303,158 @@ class TODO: "sudo systemctl enable --now erplibre.service" ) - def _qemu_erplibre_remote_cmd(self, branch, final_cmd=None, prod=False): - """Script d'installation ERPLibre exécuté DANS la VM (curl/git/make - puis clone + `final_cmd`). `final_cmd` par défaut : install_os + - install_odoo_18. `prod` : installe dans /opt/erplibre (au lieu de - ~/git/erplibre) + service SELinux confiné.""" + # GNOME et son accès distant, par gestionnaire de paquets. Une seule + # source pour la TUI et la CLI. Les noms ont été relevés distribution par + # distribution, pas déduits : Arch n'a PAS xrdp dans ses dépôts officiels + # (AUR seulement) et prend TigerVNC, avec un port et un client différents. + _QEMU_DESKTOP = { + "apt": { + "packages": "gnome-core xrdp dbus-x11", + "groups": "", + "remote": "xrdp", + "port": 3389, + "client": "RDP", + }, + "dnf": { + "packages": "xrdp", + "groups": "gnome-desktop", + "remote": "xrdp", + "port": 3389, + "client": "RDP", + }, + "pacman": { + "packages": "gdm tigervnc", + "groups": "gnome", + "remote": "", + "port": 5901, + "client": "VNC", + }, + } + # Place que prend un bureau complet, annoncée dans le plan : sur une image + # cloud de 40 G, l'oublier remplit le disque en pleine installation. + QEMU_DESKTOP_EXTRA_DISK_GB = 6 + + @staticmethod + def _qemu_cloud_init_wait(): + """Attend la fin de cloud-init, qui tient le verrou apt/dnf/pacman + pendant sa phase « paquets ». + + L'attente dure jusqu'à 15 min et n'écrivait RIEN : sur une architecture + émulée, le log restait muet un quart d'heure juste après avoir annoncé + le début de l'installation, ce qui se lit comme un blocage. Deux lignes + l'encadrent, et le « status » final dit si elle a abouti ou expiré.""" + return ( + "if command -v cloud-init >/dev/null 2>&1; then " + 'echo "== ' + + t("Waiting for cloud-init to finish (up to 15 min)") + + ' =="; ' + "sudo timeout 900 cloud-init status --wait >/dev/null 2>&1 " + "|| true; " + + f'echo " {t("cloud-init:")} $(cloud-init status 2>/dev/null ' + '| head -1)"; ' + "fi; " + ) + + @staticmethod + def _qemu_no_auto_upgrade(prod): + """Coupe les mises à jour automatiques sur une VM de DÉVELOPPEMENT. + + Vécu sur erplibre-ubuntu-2404 : unattended-upgrades s'est déclenché en + pleine migration Odoo 12->13 et a redémarré le cluster PostgreSQL + (« received fast shutdown request » x3) -> OpenUpgrade a perdu sa + connexion et la base intermédiaire est restée à moitié migrée. Effet + secondaire bienvenu : les timers apt-daily ne tiennent plus le verrou + apt pendant l'installation. En PROD on ne touche à rien : les + correctifs de sécurité automatiques doivent rester actifs.""" + if prod: + return "" + return ( + "if command -v apt-get >/dev/null 2>&1; then " + "sudo systemctl disable --now unattended-upgrades.service " + "apt-daily.timer apt-daily-upgrade.timer " + ">/dev/null 2>&1 || true; " + 'printf \'APT::Periodic::Update-Package-Lists "0";\\n' + 'APT::Periodic::Unattended-Upgrade "0";\\n\' ' + "| sudo tee /etc/apt/apt.conf.d/99-erplibre-no-auto-upgrade " + ">/dev/null; " + "fi; " + "if command -v dnf >/dev/null 2>&1; then " + "sudo systemctl disable --now dnf-automatic.timer " + "dnf-automatic-install.timer >/dev/null 2>&1 || true; " + "fi; " + # snapd : 57 s sur le CHEMIN CRITIQUE du démarrage, mesurés par + # « systemd-analyze critical-chain » sur une VM s390x — + # multi-user.target attend snapd.seeded. Aucune VM ERPLibre + # n'installe de snap : c'est du temps payé pour rien, à chaque + # démarrage. On désactive plutôt que désinstaller, pour rester + # réversible d'un « systemctl enable ». + "sudo systemctl disable --now snapd.seeded.service " + "snapd.service snapd.socket snapd.apparmor.service " + ">/dev/null 2>&1 || true; " + ) + + def _qemu_desktop_remote_cmd(self): + """Bloc shell installant GNOME + son accès distant, quelle que soit la + distribution. Même aiguillage que l'installation ERPLibre, et même + traitement du verrou apt : cette étape passe par la commande distante + et non par cloud-init, où ses 1 à 2 Go allongeraient un démarrage déjà + long sans laisser la moindre trace dans le suivi.""" + apt = self._QEMU_DESKTOP["apt"] + dnf = self._QEMU_DESKTOP["dnf"] + pac = self._QEMU_DESKTOP["pacman"] + return ( + f'echo "== {t("Installing the GNOME desktop (long)")} =="; ' + "if command -v apt-get >/dev/null 2>&1; then " + "n=0; until sudo apt-get -o DPkg::Lock::Timeout=120 update -qq; do " + "n=$((n+1)); [ $n -ge 30 ] && break; sleep 10; done; " + "sudo DEBIAN_FRONTEND=noninteractive " + "apt-get -o DPkg::Lock::Timeout=600 install -y " + f"{apt['packages']}; " + "elif command -v dnf >/dev/null 2>&1; then " + f"sudo dnf -y group install {dnf['groups']}; " + f"sudo dnf install -y {dnf['packages']}; " + "elif command -v pacman >/dev/null 2>&1; then " + "pgrep -x pacman >/dev/null 2>&1 " + "|| sudo rm -f /var/lib/pacman/db.lck; " + f"sudo pacman -S --needed --noconfirm {pac['groups']} " + f"{pac['packages']}; " + "else echo 'Gestionnaire de paquets inconnu'; exit 1; fi; " + # Le bureau ne sert à rien s'il ne démarre pas tout seul : les + # images cloud démarrent en multi-user.target. + "sudo systemctl set-default graphical.target || true; " + "sudo systemctl enable gdm >/dev/null 2>&1 || true; " + # xrdp là où il existe ; sur Arch c'est TigerVNC, qui se configure + # par utilisateur et n'a pas de service à activer d'office. + "if command -v xrdp >/dev/null 2>&1; then " + "sudo systemctl enable --now xrdp >/dev/null 2>&1 || true; " + f'echo " {t("Remote desktop:")} RDP 3389"; ' + "elif command -v vncserver >/dev/null 2>&1; then " + f'echo " {t("Remote desktop:")} VNC 5901 ' + '(vncpasswd puis vncserver :1)"; fi; ' + ) + + def _qemu_erplibre_remote_cmd( + self, branch, final_cmd=None, prod=False, desktop=False + ): + """Script exécuté DANS la VM. `branch` à None n'installe QUE le bureau + — le choix graphique ne dépend pas d'ERPLibre, et une VM peut être + voulue en bureau seul. + + `final_cmd` par défaut : install_os + install_odoo_18. `prod` : + installe dans /opt/erplibre (au lieu de ~/git/erplibre) + service + SELinux confiné. `desktop` : ajoute GNOME et son accès distant.""" + if not branch: + # Bureau seul : ni clone ni make, mais on garde le prologue — + # attente de cloud-init et coupure des mises à jour automatiques, + # sans quoi le verrou apt ferait échouer l'installation du bureau. + if not desktop: + return "true" + return ( + "set -e; " + + self._qemu_cloud_init_wait() + + self._qemu_no_auto_upgrade(prod) + + self._qemu_desktop_remote_cmd() + ) if not final_cmd: final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}" # Profils AVEC Odoo (install_odoo*) uniquement : après l'install, on @@ -4331,56 +4478,16 @@ class TODO: # secondaire bienvenu : les timers apt-daily ne tiennent plus le verrou # apt pendant l'installation. En PROD on ne touche à rien : les # correctifs de sécurité automatiques doivent rester actifs. - no_auto_upgrade = "" - if not prod: - no_auto_upgrade = ( - "if command -v apt-get >/dev/null 2>&1; then " - "sudo systemctl disable --now unattended-upgrades.service " - "apt-daily.timer apt-daily-upgrade.timer " - ">/dev/null 2>&1 || true; " - 'printf \'APT::Periodic::Update-Package-Lists "0";\\n' - 'APT::Periodic::Unattended-Upgrade "0";\\n\' ' - "| sudo tee /etc/apt/apt.conf.d/99-erplibre-no-auto-upgrade " - ">/dev/null; " - "fi; " - "if command -v dnf >/dev/null 2>&1; then " - "sudo systemctl disable --now dnf-automatic.timer " - "dnf-automatic-install.timer >/dev/null 2>&1 || true; " - "fi; " - # snapd : 57 s sur le CHEMIN CRITIQUE du démarrage, mesurés par - # « systemd-analyze critical-chain » sur une VM s390x — - # multi-user.target attend snapd.seeded. Aucune VM ERPLibre - # n'installe de snap : c'est du temps payé pour rien, à chaque - # démarrage. On désactive plutôt que désinstaller, pour rester - # réversible d'un « systemctl enable ». - "sudo systemctl disable --now snapd.seeded.service " - "snapd.service snapd.socket snapd.apparmor.service " - ">/dev/null 2>&1 || true; " - ) + no_auto_upgrade = self._qemu_no_auto_upgrade(prod) return ( - "set -e; " - # Attendre la FIN de cloud-init : pendant sa phase « paquets » il - # tient le verrou apt/dnf/pacman -> sinon « unable to lock - # database » (Arch) / « Could not get lock » (apt). timeout pour ne - # pas bloquer indéfiniment si cloud-init traîne. - # - # Cette attente dure jusqu'à 15 min et n'écrivait RIEN : sur une - # architecture émulée, le log restait muet d'un quart d'heure juste - # après avoir annoncé le début de l'installation, ce qui se lit - # comme un blocage. On encadre donc l'attente de deux lignes, et le - # « status » final dit si elle a abouti ou expiré. - "if command -v cloud-init >/dev/null 2>&1; then " - + 'echo "== ' - + t("Waiting for cloud-init to finish (up to 15 min)") - + ' =="; ' - "sudo timeout 900 cloud-init status --wait >/dev/null 2>&1 " - "|| true; " - + f'echo " {t("cloud-init:")} $(cloud-init status 2>/dev/null ' - '| head -1)"; ' - "fi; " + "set -e; " + self._qemu_cloud_init_wait() # Coupé AVANT les apt-get ci-dessous : sinon apt-daily peut reprendre # le verrou entre l'attente cloud-init et l'installation. - + no_auto_upgrade + + + no_auto_upgrade + # Le bureau d'abord : il repose sur les dépôts de la distribution, + # là où l'installation ERPLibre compile longuement. Un échec ici se + # voit donc tôt plutôt qu'après une heure. + + (self._qemu_desktop_remote_cmd() if desktop else "") + # Outils d'amorçage (absents des images cloud minimales) : curl, # git, make. Chaque branche RAFRAÎCHIT d'abord les dépôts pour que # la VM soit la plus rapide possible (miroirs à jour / les plus @@ -4453,7 +4560,13 @@ class TODO: ) def _qemu_install_erplibre_monitored( - self, names, branch, ip_map=None, final_cmd=None, prod=False + self, + names, + branch, + ip_map=None, + final_cmd=None, + prod=False, + desktop=False, ): """Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre le dashboard Textual. Quitter le dashboard n'arrête pas les installs. @@ -4465,7 +4578,9 @@ class TODO: run_monitor, ) - remote = self._qemu_erplibre_remote_cmd(branch, final_cmd, prod) + remote = self._qemu_erplibre_remote_cmd( + branch, final_cmd, prod, desktop + ) try: mod = self._qemu_import_module() except Exception: @@ -4521,7 +4636,14 @@ class TODO: print(f" {t('Read the logs:')} tail -n +1 {logdir}/*.log") def _qemu_install_erplibre_vm( - self, name, ssh_key, branch, ip=None, final_cmd=None, prod=False + self, + name, + ssh_key, + branch, + ip=None, + final_cmd=None, + prod=False, + desktop=False, ): """Clone ERPLibre (branche donnée) dans la VM puis exécute la commande d'install du profil choisi (streamé). `ip` : IP déjà résolue ; @@ -4542,7 +4664,9 @@ class TODO: f"{t('SSH not reachable, ERPLibre install skipped.')}" ) return - remote = self._qemu_erplibre_remote_cmd(branch, final_cmd, prod) + remote = self._qemu_erplibre_remote_cmd( + branch, final_cmd, prod, desktop + ) ssh_opts = ( "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " "-o ConnectTimeout=15" @@ -4915,6 +5039,11 @@ class TODO: ) else: print(f" {t('ERPLibre install:')} {t('no')}") + if spec.get("desktop"): + print( + f" {t('VM type:')} " + f"{t('Graphical (server + GNOME desktop)')}" + ) print(f" {t('SSH key:')} {spec.get('ssh_key') or t('none')}") cfg = ( t("one entry per VM") if spec["add_ssh_config"] else t("untouched") @@ -4936,6 +5065,7 @@ class TODO: dry_run, timezone=None, locale=None, + desktop=False, ): """Construit la commande deploy_qemu.py d'UNE VM (utilisée pour l'aperçu dry-run ET le déploiement réel).""" @@ -4969,9 +5099,18 @@ class TODO: parts += ["--timezone", timezone] if locale: parts += ["--locale", locale] + if desktop: + parts.append("--desktop") + extra = 0 if branch: # ERPLibre dépasse le minimum : +5 Go de disque. - bigger = self._parse_disk_gb(disk) + self.ERPLIBRE_EXTRA_DISK_GB + extra += self.ERPLIBRE_EXTRA_DISK_GB + if desktop: + # GNOME et ses dépendances pèsent autant qu'ERPLibre : sans cette + # marge, le disque se remplit en pleine installation du bureau. + extra += self.QEMU_DESKTOP_EXTRA_DISK_GB + if extra: + bigger = self._parse_disk_gb(disk) + extra parts += ["--disk-size", f"{bigger}G"] parts.append("--dry-run" if dry_run else "-y") return parts @@ -4996,6 +5135,7 @@ class TODO: dry_run=dry_run, timezone=spec.get("timezone"), locale=spec.get("locale"), + desktop=bool(spec.get("desktop")), ) # ---------------------------------------------------------------- # @@ -5230,6 +5370,7 @@ class TODO: "ram_presets": self._QEMU_RAM_PRESETS, "disk_presets": self._QEMU_DISK_PRESETS, "extra_disk_gb": self.ERPLIBRE_EXTRA_DISK_GB, + "desktop_disk_gb": self.QEMU_DESKTOP_EXTRA_DISK_GB, "defaults": { "install": True, "add_ssh_config": True, @@ -5499,6 +5640,16 @@ class TODO: ) print(f" ⚠ {warn}") + def _qemu_ask_desktop(self): + """Serveur, ou serveur plus un bureau GNOME. + + Serveur par défaut : c'est ce que sert une image cloud, et le bureau + ajoute une à deux heures d'installation sur une architecture émulée.""" + print(f"\n{t('VM type:')}") + print(f" [1] {t('Server (no graphical interface)')} *") + print(f" [2] {t('Graphical (server + GNOME desktop)')}") + return input(t("Choice (number, blank = server): ")).strip() == "2" + def _qemu_host_timezone(self): """Fuseau de l'hôte. Défini une seule fois, dans deploy_qemu.py, qui est aussi ce qui l'écrit dans le cloud-config : l'invite ne peut donc @@ -5557,6 +5708,7 @@ class TODO: timezone = self._qemu_ask_timezone() locale = self._qemu_ask_locale() + desktop = self._qemu_ask_desktop() # 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM. install = None @@ -5633,6 +5785,7 @@ class TODO: "ssh_key": ssh_key, "timezone": timezone, "locale": locale, + "desktop": desktop, "install": install, "add_ssh_config": add_ssh_config, "parallelism": parallelism, @@ -5700,6 +5853,7 @@ class TODO: deployed = list(spec.get("existing") or []) install = spec.get("install") install_branch = install["branch"] if install else None + desktop = bool(spec.get("desktop")) ssh_key = spec.get("ssh_key") add_ssh_config = spec["add_ssh_config"] parallelism = spec["parallelism"] @@ -5741,7 +5895,9 @@ class TODO: # install) : une boucle EN SÉRIE bloquait plusieurs minutes par VM # émulée SANS sortie -> le dashboard « n'ouvrait jamais ». ip_map = {} - if deployed and (add_ssh_config or install_branch): + # `desktop` compte aussi : sans IP résolue, l'installation du bureau + # n'aurait aucune VM à joindre. + if deployed and (add_ssh_config or install_branch or desktop): labels = { nm: f"{k}/{len(deployed)}" for k, nm in enumerate(deployed, 1) } @@ -5758,16 +5914,20 @@ class TODO: name, "erplibre", ip, identity_file=identity ) - # 7) Installation ERPLibre (clone + make) si demandée. - if install: - if install["monitor"]: + # 7) Installation ERPLibre (clone + make) et/ou bureau GNOME. Le bureau + # ne dépend PAS d'ERPLibre : une VM peut être voulue graphique et nue. + # Il passe par la même commande distante, donc par le même suivi. + if install or desktop: + monitor = install["monitor"] if install else True + if monitor: # Installs détachées en parallèle + dashboard Textual. self._qemu_install_erplibre_monitored( deployed, install_branch, ip_map, - install["cmd"], - install["prod"], + install["cmd"] if install else None, + install["prod"] if install else False, + desktop=desktop, ) else: print( @@ -5782,6 +5942,7 @@ class TODO: ip_map.get(name), install["cmd"], install["prod"], + desktop=desktop, ) # Sommaire TOTAL (déploiement + résolution IP + ssh_config + install diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index afc0cd2..0c4cbf1 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2023,6 +2023,30 @@ TRANSLATIONS = { "fr": "Choix (numéro, vide = rouvrir) : ", "en": "Choice (number, blank = reopen): ", }, + "VM type:": { + "fr": "Type de VM :", + "en": "VM type:", + }, + "Server (no graphical interface)": { + "fr": "Serveur (sans interface graphique)", + "en": "Server (no graphical interface)", + }, + "Graphical (server + GNOME desktop)": { + "fr": "Graphique (serveur + bureau GNOME)", + "en": "Graphical (server + GNOME desktop)", + }, + "Choice (number, blank = server): ": { + "fr": "Choix (numero, vide = serveur) : ", + "en": "Choice (number, blank = server): ", + }, + "Installing the GNOME desktop (long)": { + "fr": "Installation du bureau GNOME (long)", + "en": "Installing the GNOME desktop (long)", + }, + "Remote desktop:": { + "fr": "Bureau distant :", + "en": "Remote desktop:", + }, "One run per install": { "fr": "une execution par installation", "en": "One run per install", From f39b2e9451fcfab044b57045cb1ee0d65d9ed8ac Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 18:46:11 -0400 Subject: [PATCH 11/41] [UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04 follows, already off the lists. The refusal lands before any apt, this script also serving existing machines. AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four architectures: the twelve "latest" URLs were opened, with no index to parse unlike Fedora. They would have booted unreachable though -- the cloud-config forced "groups: users, sudo", but the RHEL family has no sudo group, only wheel, and an unknown group makes useradd fail, hence no password and no key. The very trap already known for Debian, repeated elsewhere. Host side, EPEL and CRB are enabled: without them most -devel packages are missing, silently. The server / graphical choice gains Cinnamon, the Linux Mint desktop, from the distribution's own repositories. Mint's repository is set aside: plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x. Along the way, dnf now installs an ENVIRONMENT rather than a group -- "gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X server. --- FR --- Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x. Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur contournement ; leur accumulation, non. 18.04 suit, déjà hors des listes. Le refus tombe avant tout apt, ce script servant aussi les machines existantes. AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre architectures : les douze URL « latest » ont été ouvertes, aucun index à analyser contrairement à Fedora. Elles auraient pourtant démarré inaccessibles — le cloud-config imposait « groups: users, sudo », or la famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés : sans eux la plupart des -devel manquent, en silence. Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint, depuis les dépôts de la distribution. Le dépôt de Mint lui-même est écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui exclurait arm64 et s390x. Au passage, dnf installe désormais un ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et gnome-shell mais pas base-x, donc pas de serveur X. Assisted-by: Claude Opus 5 --- .claude/rules/08-deployment.md | 7 +- CHANGELOG.base.md | 28 +++- CHANGELOG.fr.md | 12 +- CHANGELOG.md | 12 +- README.base.md | 8 +- README.fr.md | 4 +- README.md | 4 +- doc/PRODUCTION.base.md | 4 +- doc/PRODUCTION.fr.md | 2 +- doc/PRODUCTION.md | 2 +- doc/WINDOWS_INSTALLATION.base.md | 10 +- doc/WINDOWS_INSTALLATION.fr.md | 6 +- doc/WINDOWS_INSTALLATION.md | 6 +- script/install/install_debian_dependency.sh | 91 +++++------- script/install/install_dev.sh | 14 +- script/install/install_fedora_dependency.sh | 26 ++++ script/qemu/README.base.md | 54 ++++--- script/qemu/README.fr.md | 28 ++-- script/qemu/README.md | 26 ++-- script/qemu/deploy_qemu.py | 115 +++++++++------ script/selenium/README.base.md | 4 +- script/selenium/README.fr.md | 2 +- script/selenium/README.md | 2 +- script/todo/qemu_deploy_form.py | 21 ++- script/todo/todo.py | 147 ++++++++++++-------- script/todo/todo_i18n.py | 8 ++ 26 files changed, 395 insertions(+), 248 deletions(-) diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md index d7f7935..cd8beb7 100644 --- a/.claude/rules/08-deployment.md +++ b/.claude/rules/08-deployment.md @@ -6,4 +6,9 @@ - **SSL** : Certbot pour les certificats - **DNS** : `script/deployment/update_dns_cloudflare.py` -Plateformes supportées : Ubuntu 20.04-25.04, Linux Mint 22.3, Debian 12, Arch Linux, macOS (pyenv), Windows (WSL/Docker). +Plateformes supportées : Ubuntu 24.04 / 25.10 / 26.04, Linux Mint 22.3, +Debian 12, AlmaLinux 9+, Rocky Linux 9+, Arch Linux, macOS (pyenv), +Windows (WSL/Docker). + +Ubuntu 20.04 et 22.04 sont abandonnées : pikepdf exige qpdf >= 12.2, compilé +en C++20, quand focal livre GCC 9. diff --git a/CHANGELOG.base.md b/CHANGELOG.base.md index 665c8ce..56963b3 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -58,8 +58,10 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Support Selenium grid from selenium_lib.py - Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, - Arch) on amd64, arm64 and s390x, with a menu to list, test, resize, delete - and clean up + AlmaLinux, Rocky Linux, Arch) on amd64, arm64 and s390x, with a menu to list, + test, resize, delete and clean up +- Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the + Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch - Textual interfaces: install dashboard, VM deployment form, migration resume screen; Textual is installed on demand - Navigation telemetry for TODO, as a tree, a kanban or a list @@ -109,8 +111,10 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Support de la grille Selenium depuis selenium_lib.py - Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, - Debian, Fedora, Arch) en amd64, arm64 et s390x, avec un menu pour lister, - tester, redimensionner, supprimer et nettoyer + Debian, Fedora, AlmaLinux, Rocky Linux, Arch) en amd64, arm64 et s390x, avec + un menu pour lister, tester, redimensionner, supprimer et nettoyer +- VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le + bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch - Interfaces Textual : tableau de bord d'installation, formulaire de déploiement de VM, écran de reprise de migration ; Textual s'installe à la demande @@ -143,6 +147,22 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - brin_advisor et brin_cluster : recommander et appliquer le bon index PostgreSQL pour un modèle Odoo + +## Removed + +## Retiré + + +- Ubuntu 20.04 and 22.04 support, on every architecture: pikepdf needs qpdf + 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no + `g++-10` for s390x + + + +- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures : pikepdf + réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 + et ne publie pas de `g++-10` pour s390x + ## Changed diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index e93f828..9908a6e 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -32,8 +32,10 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Support de la grille Selenium depuis selenium_lib.py - Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, - Debian, Fedora, Arch) en amd64, arm64 et s390x, avec un menu pour lister, - tester, redimensionner, supprimer et nettoyer + Debian, Fedora, AlmaLinux, Rocky Linux, Arch) en amd64, arm64 et s390x, avec + un menu pour lister, tester, redimensionner, supprimer et nettoyer +- VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le + bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch - Interfaces Textual : tableau de bord d'installation, formulaire de déploiement de VM, écran de reprise de migration ; Textual s'installe à la demande @@ -66,6 +68,12 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - brin_advisor et brin_cluster : recommander et appliquer le bon index PostgreSQL pour un modèle Odoo +## Retiré + +- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures : pikepdf + réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 + et ne publie pas de `g++-10` pour s390x + ## Modifié - Support Docker postgresql 18 diff --git a/CHANGELOG.md b/CHANGELOG.md index 103f0ea..98cdeb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,8 +32,10 @@ Recreating the virtual environment, use installation guide from tool `make`. - Support Selenium grid from selenium_lib.py - Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, - Arch) on amd64, arm64 and s390x, with a menu to list, test, resize, delete - and clean up + AlmaLinux, Rocky Linux, Arch) on amd64, arm64 and s390x, with a menu to list, + test, resize, delete and clean up +- Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the + Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch - Textual interfaces: install dashboard, VM deployment form, migration resume screen; Textual is installed on demand - Navigation telemetry for TODO, as a tree, a kanban or a list @@ -64,6 +66,12 @@ Recreating the virtual environment, use installation guide from tool `make`. - brin_advisor and brin_cluster: recommend and apply the right PostgreSQL index for an Odoo model +## Removed + +- Ubuntu 20.04 and 22.04 support, on every architecture: pikepdf needs qpdf + 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no + `g++-10` for s390x + ## Changed - Docker support postgresql 18 diff --git a/README.base.md b/README.base.md index b07de49..af2bd49 100644 --- a/README.base.md +++ b/README.base.md @@ -94,7 +94,7 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. # Supported platforms -- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux +- **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux - **macOS** : via pyenv - **Windows** : via WSL or Docker - **Mainframe** : 390x architecture @@ -103,7 +103,7 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. # Plateformes supportées -- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux +- **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux - **macOS** : via pyenv - **Windows** : via WSL ou Docker - **Mainframe** : architecture 390x @@ -276,11 +276,11 @@ cd ERPLibre -Support Ubuntu 20.04, 22.04, 24.04, 25.04 and OSX. +Support Ubuntu 24.04, 25.10, 26.04, AlmaLinux 9+, Rocky Linux 9+ and OSX. -Support Ubuntu 20.04, 22.04, 24.04, 25.04 et OSX. +Support Ubuntu 24.04, 25.10, 26.04, AlmaLinux 9+, Rocky Linux 9+ et OSX. diff --git a/README.fr.md b/README.fr.md index bd81c44..1af228c 100644 --- a/README.fr.md +++ b/README.fr.md @@ -47,7 +47,7 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. # Plateformes supportées -- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux +- **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux - **macOS** : via pyenv - **Windows** : via WSL ou Docker - **Mainframe** : architecture 390x @@ -140,7 +140,7 @@ cd ERPLibre ``` -Support Ubuntu 20.04, 22.04, 24.04, 25.04 et OSX. +Support Ubuntu 24.04, 25.10, 26.04, AlmaLinux 9+, Rocky Linux 9+ et OSX. ```bash diff --git a/README.md b/README.md index 10191a4..bf6847e 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc. # Supported platforms -- **Linux** : Ubuntu 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux +- **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux - **macOS** : via pyenv - **Windows** : via WSL or Docker - **Mainframe** : 390x architecture @@ -139,7 +139,7 @@ cd ERPLibre ``` -Support Ubuntu 20.04, 22.04, 24.04, 25.04 and OSX. +Support Ubuntu 24.04, 25.10, 26.04, AlmaLinux 9+, Rocky Linux 9+ and OSX. ```bash diff --git a/doc/PRODUCTION.base.md b/doc/PRODUCTION.base.md index 2078db9..2f29351 100644 --- a/doc/PRODUCTION.base.md +++ b/doc/PRODUCTION.base.md @@ -80,14 +80,14 @@ sudo ln -s /snap/bin/certbot /usr/bin/certbot A service is running by SystemD. You can access it with the DNS name found in `env_var.sh` -#### Ubuntu 20.04 server +#### Ubuntu 24.04 server Apply fix libpng12-0: https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html Un service est en cours d'exécution via SystemD. Vous pouvez y accéder avec le nom DNS trouvé dans `env_var.sh` -#### Serveur Ubuntu 20.04 +#### Serveur Ubuntu 24.04 Appliquer le correctif libpng12-0 : https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html diff --git a/doc/PRODUCTION.fr.md b/doc/PRODUCTION.fr.md index 0740652..8ea5fba 100644 --- a/doc/PRODUCTION.fr.md +++ b/doc/PRODUCTION.fr.md @@ -45,7 +45,7 @@ sudo ln -s /snap/bin/certbot /usr/bin/certbot Un service est en cours d'exécution via SystemD. Vous pouvez y accéder avec le nom DNS trouvé dans `env_var.sh` -#### Serveur Ubuntu 20.04 +#### Serveur Ubuntu 24.04 Appliquer le correctif libpng12-0 : https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html diff --git a/doc/PRODUCTION.md b/doc/PRODUCTION.md index 151bf89..b2fbf62 100644 --- a/doc/PRODUCTION.md +++ b/doc/PRODUCTION.md @@ -45,7 +45,7 @@ sudo ln -s /snap/bin/certbot /usr/bin/certbot A service is running by SystemD. You can access it with the DNS name found in `env_var.sh` -#### Ubuntu 20.04 server +#### Ubuntu 24.04 server Apply fix libpng12-0: https://www.linuxuprising.com/2018/05/fix-libpng12-0-missing-in-ubuntu-1804.html diff --git a/doc/WINDOWS_INSTALLATION.base.md b/doc/WINDOWS_INSTALLATION.base.md index 8c8f054..44201e4 100644 --- a/doc/WINDOWS_INSTALLATION.base.md +++ b/doc/WINDOWS_INSTALLATION.base.md @@ -8,7 +8,7 @@ A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004 and up or Windows 11. There are two methods of installation, one is automatic and the other manual. -**"WSL2 Ubuntu 22.04" will be referred as "WSL2"** +**"WSL2 Ubuntu 24.04" will be referred as "WSL2"** **"PyCharm Professional" will be referred as "PyCharm"** @@ -21,7 +21,7 @@ Run Powershell with administrator rights and run the following command: Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows 10 version 2004 et plus ou Windows 11. Il existe deux méthodes d'installation, une automatique et l'autre manuelle. -**"WSL2 Ubuntu 22.04" sera désigné par "WSL2"** +**"WSL2 Ubuntu 24.04" sera désigné par "WSL2"** **"PyCharm Professional" sera désigné par "PyCharm"** @@ -31,7 +31,7 @@ Exécutez Powershell avec les droits administrateur et lancez la commande suivan ```bash -wsl --install -d Ubuntu-22.04 +wsl --install -d Ubuntu-24.04 ``` @@ -406,12 +406,12 @@ https://www.jetbrains.com/pycharm/download/#section=windows ### Set up Pycharm -Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-22.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`. +Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-24.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`. ### Configurer Pycharm -Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-22.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`. +Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-24.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`. ![Welcome to PyCharm Wizard](image/remote_development.png) diff --git a/doc/WINDOWS_INSTALLATION.fr.md b/doc/WINDOWS_INSTALLATION.fr.md index 16c3851..a6a2b49 100644 --- a/doc/WINDOWS_INSTALLATION.fr.md +++ b/doc/WINDOWS_INSTALLATION.fr.md @@ -3,7 +3,7 @@ Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows 10 version 2004 et plus ou Windows 11. Il existe deux méthodes d'installation, une automatique et l'autre manuelle. -**"WSL2 Ubuntu 22.04" sera désigné par "WSL2"** +**"WSL2 Ubuntu 24.04" sera désigné par "WSL2"** **"PyCharm Professional" sera désigné par "PyCharm"** @@ -12,7 +12,7 @@ Un guide pour configurer un espace de travail et exécuter ERPLibre sur Windows Exécutez Powershell avec les droits administrateur et lancez la commande suivante : ```bash -wsl --install -d Ubuntu-22.04 +wsl --install -d Ubuntu-24.04 ``` Si vous avez des difficultés à ouvrir Powershell avec les droits administrateur, appuyez sur `Windows + R`, entrez la ligne suivante et appuyez sur `OK`. Les droits administrateur vous seront automatiquement demandés. @@ -220,7 +220,7 @@ https://www.jetbrains.com/pycharm/download/#section=windows ### Configurer Pycharm -Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-22.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`. +Sélectionnez `Connect to WSL` sous `Remote Development`. Ensuite sélectionnez votre instance Ubuntu ("Ubuntu-24.04"). Pointez `Project directory` vers la racine du projet. Une fois que tout a été sélectionné et rempli correctement, cliquez sur `Start IDE and Connect`. ![Welcome to PyCharm Wizard](image/remote_development.png) diff --git a/doc/WINDOWS_INSTALLATION.md b/doc/WINDOWS_INSTALLATION.md index b2e87a4..912ef51 100644 --- a/doc/WINDOWS_INSTALLATION.md +++ b/doc/WINDOWS_INSTALLATION.md @@ -3,7 +3,7 @@ A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004 and up or Windows 11. There are two methods of installation, one is automatic and the other manual. -**"WSL2 Ubuntu 22.04" will be referred as "WSL2"** +**"WSL2 Ubuntu 24.04" will be referred as "WSL2"** **"PyCharm Professional" will be referred as "PyCharm"** @@ -12,7 +12,7 @@ A guide on how to set up a workspace and run ERPLibre on Windows 10 version 2004 Run Powershell with administrator rights and run the following command: ```bash -wsl --install -d Ubuntu-22.04 +wsl --install -d Ubuntu-24.04 ``` If you have trouble opening the Powershell with administrator rights, press `Windows + R`, enter the following line and press `OK`. You will be automatically prompted for administrator rights. @@ -220,7 +220,7 @@ https://www.jetbrains.com/pycharm/download/#section=windows ### Set up Pycharm -Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-22.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`. +Select `Connect to WSL` under `Remote Development`. After that select your Ubuntu instace ("Ubuntu-24.04"). Point `Project directory` to the root of the project. Once everything has been selected and filled out correctly, click on `Start IDE and Connect`. ![Welcome to PyCharm Wizard](image/remote_development.png) diff --git a/script/install/install_debian_dependency.sh b/script/install/install_debian_dependency.sh index c7e6a6f..8f1b15d 100755 --- a/script/install/install_debian_dependency.sh +++ b/script/install/install_debian_dependency.sh @@ -19,22 +19,35 @@ APT_GET="sudo apt-get -o DPkg::Lock::Timeout=600" UBUNTU_VERSION=$(lsb_release -rs) DEBIAN_VERSION=$(lsb_release -cs) OS=$(lsb_release -si) + +# Ubuntu 18.04, 20.04 et 22.04 ne sont plus supportées, sur AUCUNE +# architecture. Le mur le plus net est pikepdf, qui réclame qpdf >= 12.2, +# lui-même en C++20 : focal livre GCC 9 et ne publie même pas de g++-10 pour +# s390x. S'y ajoutaient Python 3.8 à l'amorçage, node 10, cargo 0.67 et +# OpenSSL 1.1.1 — chacun avait son contournement, l'accumulation non. +# +# Le refus est ICI, avant tout le reste : ce script tourne aussi sur une +# machine existante, pas seulement sur une VM fraîchement déployée. if [[ "${OS}" == "Ubuntu" ]]; then - if [ "20.04" == "${UBUNTU_VERSION}" ]; then - WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb - elif [ "18.04" == "${UBUNTU_VERSION}" ]; then - WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.bionic_amd64.deb - else - # 22.04+ (jusqu'à 26.04 et au-delà) : wkhtmltopdf ne publie pas de build - # par version ; le .deb « jammy » est le plus récent et fonctionne. Un - # « else » (au lieu d'énumérer les versions) évite une URL VIDE sur une - # version récente (26.04) -> gdebi appelé sans fichier -> échec. - WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb - fi + case "${UBUNTU_VERSION}" in + 18.04 | 20.04 | 22.04) + echo "Ubuntu ${UBUNTU_VERSION} n'est plus supporte par ERPLibre :" + echo " sa chaine d'outils est trop ancienne (pikepdf exige qpdf 12.2," + echo " compile en C++20, quand cette version livre GCC 9)." + echo " Utilisez Ubuntu 24.04, 25.10 ou 26.04." + exit 1 + ;; + esac +fi + +if [[ "${OS}" == "Ubuntu" ]]; then + # wkhtmltopdf ne publie pas de build par version d'Ubuntu ; le .deb + # « jammy » est le plus récent et fonctionne de 24.04 à 26.04 et au-delà. + WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb elif [[ "${OS}" == "Linuxmint" ]]; then - if [ "22.3" == "${UBUNTU_VERSION}" ]; then - WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb - fi + # Sans « else », toute Mint autre que 22.3 laissait WKHTMLTOX_X64 VIDE, et + # gdebi etait appele sans fichier. Mint 22.x repose sur noble : meme .deb. + WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb elif [[ "${OS}" == "Debian" ]]; then if [ "bullseye" == "${DEBIAN_VERSION}" ]; then WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bullseye_amd64.deb @@ -46,10 +59,10 @@ elif [[ "${OS}" == "Debian" ]]; then WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.bookworm_amd64.deb fi elif [[ "${OS}" == *"Ubuntu"* ]]; then - echo "Your version of Ubuntu is not supported, only support 18.04, 20.04 and 22.04" + echo "Your version of Ubuntu is not supported, only support 24.04, 25.10 and 26.04" WKHTMLTOX_X64=https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_amd64.deb else - echo "Your version of Ubuntu is not supported, only support 18.04, 20.04 and 22.04" + echo "Your version of Ubuntu is not supported, only support 24.04, 25.10 and 26.04" exit 1 fi @@ -58,25 +71,6 @@ fi #-------------------------------------------------- if [ "$(uname -m)" = "s390x" ]; then echo "Arch s390x detected" - # Sur s390x, aucune roue PyPI : tout se compile contre les bibliothèques de - # la distribution. Deux versions n'en ont pas d'assez récentes. - # - # 20.04 : pikepdf réclame qpdf >= 12.2, dont la compilation exige C++20. - # focal livre GCC 9 et ne publie PAS g++-10 pour s390x — vérifié. - # La compilation s'arrête sur « fatal error: concepts: No such file - # or directory », sans contournement possible. - # 22.04 : même mur qpdf, franchissable en théorie, abandonné avec la 20.04 - # plutôt que de maintenir un chemin de plus. - # - # On refuse ICI, avant une heure de compilation émulée pour rien. - case "${UBUNTU_VERSION}" in - 20.04 | 22.04) - echo "Ubuntu ${UBUNTU_VERSION} n'est plus supporte sur s390x :" - echo " sa chaine d'outils est trop ancienne pour ERPLibre." - echo " Utilisez Ubuntu 24.04 ou plus recent." - exit 1 - ;; - esac # Sur une VM fraîche, l'index apt peut être vide : « apt install » échouerait # alors sur TOUT le lot, et l'absence d'un seul paquet ne se voit que bien # plus loin, sous la forme d'une commande introuvable. @@ -239,16 +233,6 @@ fi #-------------------------------------------------- echo -e "\n---- Update Server ----" -if [ "18.04" == "${UBUNTU_VERSION}" ]; then - # add-apt-repository can install add-apt-repository Ubuntu 18.x - ${APT_GET} install software-properties-common curl -y - # universe package is for Ubuntu 18.x - sudo add-apt-repository universe - # libpng12-0 dependency for wkhtmltopdf - sudo add-apt-repository "deb http://mirrors.kernel.org/ubuntu/ xenial main" - ${APT_GET} update - ${APT_GET} upgrade -y -fi #-------------------------------------------------- # Install PostgreSQL Server @@ -292,14 +276,6 @@ if [[ $retVal -ne 0 ]]; then echo "apt-get libmariadb installation error." exit 1 fi -if [ "18.04" == "${UBUNTU_VERSION}" ]; then - ${APT_GET} install libpng12-0 -y - retVal=$? - if [[ $retVal -ne 0 ]]; then - echo "apt-get libpng installation error." - exit 1 - fi -fi # Dependencies for pyenv ${APT_GET} install make libssl-dev zlib1g-dev libreadline-dev libsqlite3-dev curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev liblzma-dev -y retVal=$? @@ -321,13 +297,8 @@ ${APT_GET} install -y ca-certificates curl gnupg sudo mkdir -p /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg -if [ "18.04" == "${UBUNTU_VERSION}" ]; then - sudo apt remove nodeJS npm - NODE_MAJOR=16 -else - # Node 22+ required by @capacitor/cli v8.x (mobile app dependency) - NODE_MAJOR=22 -fi +# Node 22+ required by @capacitor/cli v8.x (mobile app dependency) +NODE_MAJOR=22 # NodeSource ne publie que amd64, arm64 et armhf : vérifié, binary-s390x # répond 404. Ajouter le dépôt sur une autre architecture n'apporte rien et diff --git a/script/install/install_dev.sh b/script/install/install_dev.sh index 95cc0a3..9274230 100755 --- a/script/install/install_dev.sh +++ b/script/install/install_dev.sh @@ -3,11 +3,15 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then source /etc/os-release if [[ "${ID}" == "ubuntu" ]]; then - if [[ "${VERSION_ID}" == "18.04" || "${VERSION_ID}" == "20.04" || "${VERSION_ID}" == "22.04" || "${VERSION_ID}" == "22.10" || "${VERSION_ID}" == "23.04" || "${VERSION_ID}" == "23.10" || "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.04" || "${VERSION_ID}" == "25.10" || "${VERSION_ID}" == "26.04" ]]; then + # 20.04 et 22.04 retirees : leur chaine d'outils ne suffit plus a ERPLibre + # (pikepdf exige qpdf >= 12.2, en C++20, quand focal livre GCC 9). Les + # versions intermediaires EOL (22.10, 23.04, 23.10, 25.04) partent avec. + if [[ "${VERSION_ID}" == "24.04" || "${VERSION_ID}" == "25.10" || "${VERSION_ID}" == "26.04" ]]; then echo "\n---- linux-gnu installation process started ----" ./script/install/install_debian_dependency.sh else - echo "Your version is not supported, only support 18.04, 20.04 and 22.04 - 24.04, 25.04, 25.10, 26.04 : ${VERSION_ID}" + echo "Your version is not supported, only support 24.04, 25.10, 26.04 : ${VERSION_ID}" + exit 1 fi elif [[ "${ID}" == "linuxmint" ]]; then if [[ "${VERSION_ID}" == "22.3" ]]; then @@ -21,11 +25,13 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then elif [[ "${ID}" == "arch" ]]; then ./script/install/install_arch_linux.sh elif [[ "${ID}" == "fedora" || "${ID_LIKE}" == *"fedora"* || "${ID_LIKE}" == *"rhel"* ]]; then - echo "\n---- Fedora installation process started ----" + # AlmaLinux et Rocky declarent ID_LIKE="rhel centos fedora" : elles passent + # donc par ce meme script, qui aiguille dnf. + echo "\n---- Fedora/RHEL installation process started ----" ./script/install/install_fedora_dependency.sh else ./script/install/install_debian_dependency.sh - echo "Your Linux system is not supported, only support Ubuntu 18.04 or Ubuntu 20.04 or Ubuntu 22.04 - Ubuntu 23.10 - Ubuntu 24.04, Ubuntu 25.04, Ubuntu 25.10, Debian, Fedora, Arch." + echo "Your Linux system is not supported, only support Ubuntu 24.04, 25.10, 26.04, Linux Mint 22.3, Debian, Fedora, AlmaLinux, Rocky Linux, Arch." fi elif [[ "${OSTYPE}" == "darwin"* ]]; then echo "\n---- Darwin installation process started ----" diff --git a/script/install/install_fedora_dependency.sh b/script/install/install_fedora_dependency.sh index 7fbafa7..e530196 100755 --- a/script/install/install_fedora_dependency.sh +++ b/script/install/install_fedora_dependency.sh @@ -14,6 +14,32 @@ EL_USER=${USER} # signature après un cache périmé) et saute les paquets introuvables. DNF="sudo dnf install -y --refresh --skip-unavailable" +#-------------------------------------------------- +# Dérivés RHEL : dépôts supplémentaires +#-------------------------------------------------- +# AlmaLinux, Rocky, CentOS Stream et RHEL passent par ce script (install_dev.sh +# les aiguille sur ID_LIKE=rhel). Contrairement à Fedora, leur jeu de dépôts +# par défaut est ÉTROIT : la plupart des paquets « -devel » vivent dans CRB +# (CodeReady Builder), et tout ce qui est communautaire dans EPEL. Sans ces +# deux dépôts, « --skip-unavailable » saute les paquets EN SILENCE et l'échec +# n'apparaît qu'à la compilation, très loin d'ici. +if [ -r /etc/os-release ]; then + # shellcheck disable=SC1091 + . /etc/os-release +fi +case "${ID}" in + almalinux | rocky | rhel | centos) + echo -e "\n---- Depots EPEL et CRB (famille RHEL) ----" + sudo dnf install -y epel-release \ + || echo "epel-release indisponible : certains paquets manqueront." + # « crb » sur EL9/EL10 ; « powertools » était le nom sur EL8. + sudo dnf config-manager --set-enabled crb 2> /dev/null \ + || sudo dnf config-manager --set-enabled powertools 2> /dev/null \ + || sudo dnf config-manager --enable crb 2> /dev/null \ + || echo "CRB/PowerTools non active : certains -devel manqueront." + ;; +esac + #-------------------------------------------------- # Outils de compilation (build Python via pyenv, extensions Python) #-------------------------------------------------- diff --git a/script/qemu/README.base.md b/script/qemu/README.base.md index 33daefb..cca9e41 100644 --- a/script/qemu/README.base.md +++ b/script/qemu/README.base.md @@ -173,31 +173,47 @@ sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ ``` -Supported Ubuntu versions: `20.04`, `22.04`, `24.04` (default), `24.10`, -`25.04`, `25.10`. Provide an explicit image path as a positional argument to -override the automatic download location. +Catalog, per architecture (`deploy_qemu.py` is the source of truth): -On **s390x**, `20.04` and `22.04` are **not supported**: no PyPI wheel exists -for that architecture, so everything is built against the distribution's -libraries, and pikepdf needs qpdf 12.2, whose build requires C++20. Focal -ships GCC 9 and publishes no `g++-10` for s390x — there is no way around it. -Use `24.04` or later. Both are still supported on amd64 and arm64, where pip -installs prebuilt wheels. +| Distro | Versions | amd64 | arm64 | s390x | +|---|---|:-:|:-:|:-:| +| ubuntu | `24.04` (default), `25.10`, `26.04` | ✔ | ✔ | ✔ | +| debian | `11`, `12` (default), `13` | ✔ | ✔ | — | +| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | — | +| almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | +| rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | +| arch | `latest` | ✔ | — | — | + +Provide an explicit image path as a positional argument to override the +automatic download location. + +Ubuntu `20.04` and `22.04` were **dropped on every architecture**: pikepdf +needs qpdf 12.2, whose build requires C++20, and focal ships GCC 9 — it does +not even publish `g++-10` for s390x. Python 3.8, node 10, cargo 0.67 and +OpenSSL 1.1.1 each had a workaround; the pile of them did not. ## After deployment -Versions Ubuntu supportées : `20.04`, `22.04`, `24.04` (défaut), `24.10`, -`25.04`, `25.10`. Fournissez un chemin d'image en argument positionnel pour -surcharger l'emplacement de téléchargement automatique. +Catalogue, par architecture (`deploy_qemu.py` fait autorité) : -Sur **s390x**, les `20.04` et `22.04` ne sont **pas supportées** : aucune roue -PyPI n'existe pour cette architecture, tout se compile donc contre les -bibliothèques de la distribution, et pikepdf réclame qpdf 12.2, dont la -compilation exige C++20. Focal livre GCC 9 et ne publie pas de `g++-10` pour -s390x — il n'y a pas de contournement. Utilisez la `24.04` ou plus récente. -Les deux restent supportées sur amd64 et arm64, où pip pose des roues -précompilées. +| Distro | Versions | amd64 | arm64 | s390x | +|---|---|:-:|:-:|:-:| +| ubuntu | `24.04` (défaut), `25.10`, `26.04` | ✔ | ✔ | ✔ | +| debian | `11`, `12` (défaut), `13` | ✔ | ✔ | — | +| fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | — | +| almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | +| rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | +| arch | `latest` | ✔ | — | — | + +Fournissez un chemin d'image en argument positionnel pour surcharger +l'emplacement de téléchargement automatique. + +Les Ubuntu `20.04` et `22.04` sont **abandonnées sur toutes les +architectures** : pikepdf réclame qpdf 12.2, dont la compilation exige C++20, +et focal livre GCC 9 — elle ne publie même pas de `g++-10` pour s390x. Python +3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur contournement ; +leur accumulation, non. ## Après le déploiement diff --git a/script/qemu/README.fr.md b/script/qemu/README.fr.md index 21dfe7f..19c022f 100644 --- a/script/qemu/README.fr.md +++ b/script/qemu/README.fr.md @@ -92,17 +92,25 @@ sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ --ssh-key ~/.ssh/id_ed25519.pub -y ``` -Versions Ubuntu supportées : `20.04`, `22.04`, `24.04` (défaut), `24.10`, -`25.04`, `25.10`. Fournissez un chemin d'image en argument positionnel pour -surcharger l'emplacement de téléchargement automatique. +Catalogue, par architecture (`deploy_qemu.py` fait autorité) : -Sur **s390x**, les `20.04` et `22.04` ne sont **pas supportées** : aucune roue -PyPI n'existe pour cette architecture, tout se compile donc contre les -bibliothèques de la distribution, et pikepdf réclame qpdf 12.2, dont la -compilation exige C++20. Focal livre GCC 9 et ne publie pas de `g++-10` pour -s390x — il n'y a pas de contournement. Utilisez la `24.04` ou plus récente. -Les deux restent supportées sur amd64 et arm64, où pip pose des roues -précompilées. +| Distro | Versions | amd64 | arm64 | s390x | +|---|---|:-:|:-:|:-:| +| ubuntu | `24.04` (défaut), `25.10`, `26.04` | ✔ | ✔ | ✔ | +| debian | `11`, `12` (défaut), `13` | ✔ | ✔ | — | +| fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | — | +| almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | +| rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | +| arch | `latest` | ✔ | — | — | + +Fournissez un chemin d'image en argument positionnel pour surcharger +l'emplacement de téléchargement automatique. + +Les Ubuntu `20.04` et `22.04` sont **abandonnées sur toutes les +architectures** : pikepdf réclame qpdf 12.2, dont la compilation exige C++20, +et focal livre GCC 9 — elle ne publie même pas de `g++-10` pour s390x. Python +3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur contournement ; +leur accumulation, non. ## Après le déploiement diff --git a/script/qemu/README.md b/script/qemu/README.md index 9335a43..a4163a7 100644 --- a/script/qemu/README.md +++ b/script/qemu/README.md @@ -86,16 +86,24 @@ sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \ --ssh-key ~/.ssh/id_ed25519.pub -y ``` -Supported Ubuntu versions: `20.04`, `22.04`, `24.04` (default), `24.10`, -`25.04`, `25.10`. Provide an explicit image path as a positional argument to -override the automatic download location. +Catalog, per architecture (`deploy_qemu.py` is the source of truth): -On **s390x**, `20.04` and `22.04` are **not supported**: no PyPI wheel exists -for that architecture, so everything is built against the distribution's -libraries, and pikepdf needs qpdf 12.2, whose build requires C++20. Focal -ships GCC 9 and publishes no `g++-10` for s390x — there is no way around it. -Use `24.04` or later. Both are still supported on amd64 and arm64, where pip -installs prebuilt wheels. +| Distro | Versions | amd64 | arm64 | s390x | +|---|---|:-:|:-:|:-:| +| ubuntu | `24.04` (default), `25.10`, `26.04` | ✔ | ✔ | ✔ | +| debian | `11`, `12` (default), `13` | ✔ | ✔ | — | +| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | — | +| almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | +| rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | +| arch | `latest` | ✔ | — | — | + +Provide an explicit image path as a positional argument to override the +automatic download location. + +Ubuntu `20.04` and `22.04` were **dropped on every architecture**: pikepdf +needs qpdf 12.2, whose build requires C++20, and focal ships GCC 9 — it does +not even publish `g++-10` for s390x. Python 3.8, node 10, cargo 0.67 and +OpenSSL 1.1.1 each had a workaround; the pile of them did not. ## After deployment diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index 15f7d0d..21c9a72 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -79,9 +79,13 @@ from pathlib import Path # transitoire. Un VM à 10G tombait « Poetry installation error » (disque # plein). Le qcow2 est CREUX (sparse) : une taille virtuelle plus grande ne # consomme rien tant qu'elle n'est pas remplie -> 20G sans surcoût réel. +# +# 20.04 et 22.04 ne sont plus proposées : leur chaîne d'outils ne suffit plus à +# ERPLibre. Le mur le plus net est pikepdf, qui réclame qpdf >= 12.2, lui-même +# en C++20 — focal livre GCC 9. S'y ajoutaient Python 3.8 à l'amorçage, node 10, +# cargo 0.67 et OpenSSL 1.1.1. Chacun avait son contournement ; l'accumulation, +# non. UBUNTU_VERSIONS: dict[str, tuple[str, str, int, str]] = { - "20.04": ("focal", "ubuntu20.04", 2048, "20G"), - "22.04": ("jammy", "ubuntu22.04", 2048, "20G"), "24.04": ("noble", "ubuntu24.04", 3072, "20G"), "25.10": ("questing", "ubuntu25.10", 3072, "20G"), "26.04": ("resolute", "ubuntu26.04", 3072, "20G"), @@ -97,6 +101,18 @@ FEDORA_VERSIONS: dict[str, tuple[str, str, int, str]] = { "43": ("43", "fedora43", 2048, "20G"), "44": ("44", "fedora44", 2048, "20G"), } +# Dérivés RHEL. Le second champ est l'identifiant libosinfo : « almalinux9 » et +# « almalinux10 » figurent dans osinfo-db, « rocky10 » n'y est pas encore sur +# les hôtes 24.04 — osinfo_arg() replie alors sur une valeur connue. +# RAM : libosinfo demande 1,5 Gio en x86_64 ; on retient 2048 comme Fedora. +ALMALINUX_VERSIONS: dict[str, tuple[str, str, int, str]] = { + "9": ("9", "almalinux9", 2048, "20G"), + "10": ("10", "almalinux10", 2048, "20G"), +} +ROCKY_VERSIONS: dict[str, tuple[str, str, int, str]] = { + "9": ("9", "rocky9", 2048, "20G"), + "10": ("10", "rocky10", 2048, "20G"), +} # Arch est en rolling release : une seule « version » (latest). ARCH_VERSIONS: dict[str, tuple[str, str, int, str]] = { "latest": ("latest", "archlinux", 1024, "20G"), @@ -107,6 +123,8 @@ DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = { "ubuntu": (UBUNTU_VERSIONS, "24.04"), "debian": (DEBIAN_VERSIONS, "12"), "fedora": (FEDORA_VERSIONS, "42"), + "almalinux": (ALMALINUX_VERSIONS, "9"), + "rocky": (ROCKY_VERSIONS, "10"), "arch": (ARCH_VERSIONS, "latest"), } @@ -114,6 +132,8 @@ DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = { # s390x s'écrit « s390x » partout (identité), donc aucune entrée n'est requise. ARCH_ALIASES: dict[str, dict[str, str]] = { "fedora": {"amd64": "x86_64", "arm64": "aarch64"}, + "almalinux": {"amd64": "x86_64", "arm64": "aarch64"}, + "rocky": {"amd64": "x86_64", "arm64": "aarch64"}, "arch": {"amd64": "x86_64", "arm64": "aarch64"}, } @@ -126,31 +146,14 @@ NON_X86_ARCHES: tuple[str, ...] = ("arm64", "s390x") # - s390x (IBM Z) : Ubuntu seulement (Debian/Fedora : 404 ; Arch : x86/arm). # - arm64/aarch64 : Ubuntu, Debian, Fedora (Arch : pas d'image cloud officielle # aarch64 sur geo.mirror.pkgbuild.com). -S390X_DISTROS: tuple[str, ...] = ("ubuntu",) -ARM64_DISTROS: tuple[str, ...] = ("ubuntu", "debian", "fedora") - -# Versions ABANDONNÉES sur une architecture donnée : publier une image cloud ne -# suffit pas, encore faut-il que la chaîne d'outils permette d'installer -# ERPLibre. Sur s390x, aucune roue PyPI n'existe : tout se compile contre les -# bibliothèques de la distribution, et deux versions n'en ont pas d'assez -# récentes. -# -# 20.04 : pikepdf réclame qpdf >= 12.2, dont la compilation exige C++20 — -# or focal livre GCC 9 et NE PUBLIE PAS g++-10 pour s390x. Sans -# issue. S'y ajoutaient Python 3.8, node 10, cargo 0.67 et -# OpenSSL 1.1.1, tous contournés, mais celui-là ne l'est pas. -# 22.04 : même mur qpdf, franchissable en théorie (GCC 11), abandonné avec -# la 20.04 pour ne pas maintenir un chemin de plus. -# -# amd64 et arm64 ne sont PAS concernés : pip y pose des roues précompilées. -UNSUPPORTED_VERSIONS: dict[str, dict[str, tuple[str, ...]]] = { - "s390x": {"ubuntu": ("20.04", "22.04")}, -} - - -def is_supported(distro: str, version: str, arch: str) -> bool: - """Faux si ce triplet est explicitement abandonné.""" - return version not in UNSUPPORTED_VERSIONS.get(arch, {}).get(distro, ()) +S390X_DISTROS: tuple[str, ...] = ("ubuntu", "almalinux", "rocky") +ARM64_DISTROS: tuple[str, ...] = ( + "ubuntu", + "debian", + "fedora", + "almalinux", + "rocky", +) # arch générique -> distros la publiant (pour valider --arch tôt). @@ -187,6 +190,8 @@ DEBIAN_CLOUD_BASES: tuple[str, ...] = ( "https://gemmei.ftp.acc.umu.se/cdimage/cloud", "https://laotzu.ftp.acc.umu.se/cdimage/cloud", ) +ALMALINUX_CLOUD_BASE = "https://repo.almalinux.org/almalinux" +ROCKY_CLOUD_BASE = "https://dl.rockylinux.org/pub/rocky" FEDORA_BASE = "https://download.fedoraproject.org/pub/fedora/linux/releases" # Serveur MAÎTRE (pas de redirection MirrorManager) : repli fiable quand le # redirecteur envoie sur un miroir incomplet (fréquent en déploiement @@ -237,6 +242,22 @@ def image_candidates( ] if distro == "fedora": return [resolve_fedora_url(version, arch, dry_run)] + if distro == "almalinux": + # Contrairement à Fedora, AlmaLinux publie un lien « latest » STABLE + # par majeure et par architecture : aucun index HTML à analyser. On + # vise l'arbre de la MAJEURE (9/, 10/), qui suit la mineure courante — + # les répertoires mineurs périmés sont retirés du miroir. + return [ + f"{ALMALINUX_CLOUD_BASE}/{version}/cloud/{a}/images/" + f"AlmaLinux-{version}-GenericCloud-latest.{a}.qcow2" + ] + if distro == "rocky": + # Même principe : l'alias « .latest » suit le point release courant, et + # les précédents partent au vault — une URL figée casserait. + return [ + f"{ROCKY_CLOUD_BASE}/{version}/images/{a}/" + f"Rocky-{version}-GenericCloud.latest.{a}.qcow2" + ] if distro == "arch": # Rolling release : image « latest » officielle (cloud-init inclus). return [f"{ARCH_CLOUD_BASE}/Arch-Linux-{a}-cloudimg.qcow2"] @@ -290,6 +311,12 @@ def default_image_name(distro: str, code: str, arch: str, version: str) -> str: return f"debian-{version}-genericcloud-{a}.qcow2" if distro == "arch": return f"arch-linux-{a}-cloudimg.qcow2" + if distro in ("almalinux", "rocky"): + # « latest » est MUTABLE : le nom de cache porte donc la majeure, et + # une image déjà téléchargée sera réutilisée telle quelle. C'est voulu + # (reproductibilité d'un déploiement à l'autre) ; supprimer le fichier + # du cache suffit à repartir sur le build courant. + return f"{distro}-{version}-genericcloud-{a}.qcow2" return f"fedora-cloud-{version}-{a}.qcow2" @@ -1312,6 +1339,14 @@ def host_timezone() -> str: return "UTC" +def admin_group(distro: str) -> str: + """Groupe d'administration de la distribution. + + Debian et Ubuntu : « sudo ». Famille RHEL (AlmaLinux, Rocky, Fedora) et + Arch : « wheel » — elles n'ont aucun groupe « sudo ».""" + return "sudo" if distro in ("ubuntu", "debian") else "wheel" + + def build_cloud_config( args: argparse.Namespace, pw_hash: str | None, ssh_keys: list[str] ) -> str: @@ -1322,10 +1357,15 @@ def build_cloud_config( "users:", f" - name: {args.user}", " sudo: ALL=(ALL) NOPASSWD:ALL", - # « sudo » existe sur Debian ET Ubuntu ; « admin » n'existe PAS sur - # Debian -> useradd -G ...,admin échoue et l'utilisateur n'est jamais - # créé (login/clé SSH KO). C'était la cause du « Debian ne marche pas ». - " groups: users, sudo", + # Le groupe d'administration N'A PAS le même nom partout, et un nom + # inconnu fait échouer « useradd -G » : l'utilisateur n'est alors jamais + # créé, donc ni mot de passe ni clé SSH — la VM démarre et reste + # inaccessible. C'était déjà la cause du « Debian ne marche pas » + # (« admin » n'existe pas sur Debian) ; la famille RHEL et Arch ont le + # même écart, elles n'ont pas de groupe « sudo » mais « wheel ». + # Le privilège lui-même vient de la ligne « sudo: » ci-dessus, pas du + # groupe : celui-ci n'est qu'une commodité. + f" groups: users, {admin_group(args.distro)}", " shell: /bin/bash", " lock_passwd: false" if pw_hash else " lock_passwd: true", ] @@ -2133,19 +2173,6 @@ def main() -> None: f"Architecture {args.arch} indisponible pour {args.distro!r} : " f"images cloud publiées seulement pour {', '.join(supported)}." ) - # L'image cloud existe, mais la chaîne d'outils de cette version ne permet - # pas d'installer ERPLibre sur cette architecture. Mieux vaut le dire ici - # qu'après une heure de compilation émulée. - if not is_supported(args.distro, args.version, args.arch): - dropped = UNSUPPORTED_VERSIONS[args.arch][args.distro] - keep = [v for v in versions if v not in dropped] - sys.exit( - f"{args.distro} {args.version} n'est plus supporté sur " - f"{args.arch} : sa chaîne d'outils est trop ancienne pour " - f"ERPLibre (GCC 9 sur 20.04 ne compile pas le qpdf que réclame " - f"pikepdf, et aucun g++ plus récent n'y est publié).\n" - f" Versions supportées sur {args.arch} : {', '.join(keep)}." - ) code, default_osinfo, min_ram, min_disk = versions[args.version] if args.codename: code = args.codename diff --git a/script/selenium/README.base.md b/script/selenium/README.base.md index cb90344..07b21ce 100644 --- a/script/selenium/README.base.md +++ b/script/selenium/README.base.md @@ -4,10 +4,10 @@ -# Installation Ubuntu 20.04 +# Installation Ubuntu 24.04 -# Installation Ubuntu 20.04 +# Installation Ubuntu 24.04 sudo apt install libcairo2-dev diff --git a/script/selenium/README.fr.md b/script/selenium/README.fr.md index 023d8c9..b813826 100644 --- a/script/selenium/README.fr.md +++ b/script/selenium/README.fr.md @@ -1,4 +1,4 @@ -# Installation Ubuntu 20.04 +# Installation Ubuntu 24.04 sudo apt install libcairo2-dev \ No newline at end of file diff --git a/script/selenium/README.md b/script/selenium/README.md index 023d8c9..b813826 100644 --- a/script/selenium/README.md +++ b/script/selenium/README.md @@ -1,4 +1,4 @@ -# Installation Ubuntu 20.04 +# Installation Ubuntu 24.04 sudo apt install libcairo2-dev \ No newline at end of file diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 264095e..7f75bd8 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -177,7 +177,7 @@ def build_spec(vms, domains, form): "existing": [vm["name"] for vm in vms if vm["name"] in known], "ssh_key": form["ssh_key"], "timezone": form.get("timezone", ""), - "desktop": form.get("desktop", False), + "desktop": form.get("desktop", ""), "install": form["install"], "add_ssh_config": form["add_ssh_config"], "parallelism": form["parallelism"], @@ -243,6 +243,8 @@ def run_deploy_form(ctx, run_app: bool = True): base_vcpus = ctx.get("base_vcpus") or 2 extra_disk = ctx.get("extra_disk_gb") or 0 desktop_disk = ctx.get("desktop_disk_gb") or 0 + # [(clé, libellé)] — la liste vient de todo.py, source unique. + desktops = list(ctx.get("desktops") or []) defaults = ctx.get("defaults") or {} result = {"spec": None} @@ -452,12 +454,13 @@ def run_deploy_form(ctx, run_app: bool = True): with RadioSet(id="f_type"): yield RadioButton( t("Server (no graphical interface)"), - value=not defaults.get("desktop", False), - ) - yield RadioButton( - t("Graphical (server + GNOME desktop)"), - value=defaults.get("desktop", False), + value=not defaults.get("desktop", ""), ) + for key, label in desktops: + yield RadioButton( + f"{t('Graphical (server + desktop):')} {label}", + value=defaults.get("desktop", "") == key, + ) yield Static("ERPLibre", classes="grouptitle") yield Checkbox( t("Install ERPLibre"), @@ -599,7 +602,11 @@ def run_deploy_form(ctx, run_app: bool = True): self._render_plan() def _desktop(self): - return self.query_one("#f_type", RadioSet).pressed_index == 1 + """« » pour un serveur, sinon la clé de la saveur choisie.""" + index = self.query_one("#f_type", RadioSet).pressed_index + if index is None or index < 1 or index > len(desktops): + return "" + return desktops[index - 1][0] def _render_plan(self): table = self.query_one("#plan", DataTable) diff --git a/script/todo/todo.py b/script/todo/todo.py index d806595..b9bf152 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1022,12 +1022,11 @@ class TODO: # distro -> (versions affichées, version par défaut). Source de vérité = # deploy_qemu.py ; ceci ne sert qu'au sélecteur interactif. _QEMU_DISTROS = { - "ubuntu": ( - ["20.04", "22.04", "24.04", "25.10", "26.04"], - "24.04", - ), + "ubuntu": (["24.04", "25.10", "26.04"], "24.04"), "debian": (["11", "12", "13"], "12"), "fedora": (["41", "42", "43", "44"], "42"), + "almalinux": (["9", "10"], "9"), + "rocky": (["9", "10"], "10"), "arch": (["latest"], "latest"), } @@ -1075,8 +1074,14 @@ class TODO: # Distros publiant des images cloud par architecture (cohérent avec # S390X_DISTROS / ARM64_DISTROS de deploy_qemu.py). amd64 : toutes. - _QEMU_S390X_DISTROS = ("ubuntu",) - _QEMU_ARM64_DISTROS = ("ubuntu", "debian", "fedora") + _QEMU_S390X_DISTROS = ("ubuntu", "almalinux", "rocky") + _QEMU_ARM64_DISTROS = ( + "ubuntu", + "debian", + "fedora", + "almalinux", + "rocky", + ) # Alias distro pour l'affichage (jeton générique -> nom courant). _QEMU_ARCH_ALIAS = {"amd64": "x86_64", "arm64": "aarch64"} @@ -4303,33 +4308,43 @@ class TODO: "sudo systemctl enable --now erplibre.service" ) - # GNOME et son accès distant, par gestionnaire de paquets. Une seule - # source pour la TUI et la CLI. Les noms ont été relevés distribution par + # Bureaux disponibles, par gestionnaire de paquets. Une seule source pour + # la TUI et la CLI. Les noms ont été relevés distribution par # distribution, pas déduits : Arch n'a PAS xrdp dans ses dépôts officiels # (AUR seulement) et prend TigerVNC, avec un port et un client différents. + # + # Côté dnf on installe un ENVIRONNEMENT, pas un groupe : le groupe + # « gnome-desktop » d'AlmaLinux apporte gdm et gnome-shell mais PAS + # « base-x », donc aucun serveur X — vérifié dans son comps.xml. Les + # environnements diffèrent selon la famille (RHEL / Fedora), d'où la + # cascade : le premier qui existe gagne. _QEMU_DESKTOP = { - "apt": { - "packages": "gnome-core xrdp dbus-x11", - "groups": "", - "remote": "xrdp", - "port": 3389, - "client": "RDP", + "gnome": { + "label": "GNOME", + "apt": "gnome-core dbus-x11", + "dnf_env": "graphical-server-environment " + "workstation-product-environment gnome-desktop", + "pacman": "gnome gdm", + "service": "gdm", }, - "dnf": { - "packages": "xrdp", - "groups": "gnome-desktop", - "remote": "xrdp", - "port": 3389, - "client": "RDP", - }, - "pacman": { - "packages": "gdm tigervnc", - "groups": "gnome", - "remote": "", - "port": 5901, - "client": "VNC", + "cinnamon": { + "label": "Cinnamon (Linux Mint)", + # Le bureau de Linux Mint, depuis les dépôts de la distribution : + # Ubuntu 24.04 livre Cinnamon 6.0.4, la 25.10 la 6.4.12. Le dépôt + # de Mint lui-même n'est pas utilisé — il est en HTTP nu et ne + # publie que i386/amd64, ce qui exclurait arm64 et s390x. + "apt": "cinnamon-desktop-environment dbus-x11", + "dnf_env": "cinnamon-desktop", + "pacman": "cinnamon lightdm lightdm-gtk-greeter", + "service": "lightdm", }, } + # Accès distant, indépendant du bureau choisi. + _QEMU_DESKTOP_REMOTE = { + "apt": {"packages": "xrdp", "port": 3389, "client": "RDP"}, + "dnf": {"packages": "xrdp", "port": 3389, "client": "RDP"}, + "pacman": {"packages": "tigervnc", "port": 5901, "client": "VNC"}, + } # Place que prend un bureau complet, annoncée dans le plan : sur une image # cloud de 40 G, l'oublier remplit le disque en pleine installation. QEMU_DESKTOP_EXTRA_DISK_GB = 6 @@ -4393,36 +4408,43 @@ class TODO: ">/dev/null 2>&1 || true; " ) - def _qemu_desktop_remote_cmd(self): - """Bloc shell installant GNOME + son accès distant, quelle que soit la - distribution. Même aiguillage que l'installation ERPLibre, et même - traitement du verrou apt : cette étape passe par la commande distante - et non par cloud-init, où ses 1 à 2 Go allongeraient un démarrage déjà - long sans laisser la moindre trace dans le suivi.""" - apt = self._QEMU_DESKTOP["apt"] - dnf = self._QEMU_DESKTOP["dnf"] - pac = self._QEMU_DESKTOP["pacman"] + def _qemu_desktop_remote_cmd(self, flavour="gnome"): + """Bloc shell installant le bureau choisi + son accès distant, quelle + que soit la distribution. Même aiguillage que l'installation ERPLibre, + et même traitement du verrou apt : cette étape passe par la commande + distante et non par cloud-init, où ses 1 à 2 Go allongeraient un + démarrage déjà long sans laisser la moindre trace dans le suivi.""" + de = self._QEMU_DESKTOP.get(flavour) or self._QEMU_DESKTOP["gnome"] + rem = self._QEMU_DESKTOP_REMOTE + label = de["label"] return ( - f'echo "== {t("Installing the GNOME desktop (long)")} =="; ' + f'echo "== {t("Installing the desktop (long):")} {label} =="; ' "if command -v apt-get >/dev/null 2>&1; then " "n=0; until sudo apt-get -o DPkg::Lock::Timeout=120 update -qq; do " "n=$((n+1)); [ $n -ge 30 ] && break; sleep 10; done; " "sudo DEBIAN_FRONTEND=noninteractive " "apt-get -o DPkg::Lock::Timeout=600 install -y " - f"{apt['packages']}; " + f"{de['apt']} {rem['apt']['packages']}; " "elif command -v dnf >/dev/null 2>&1; then " - f"sudo dnf -y group install {dnf['groups']}; " - f"sudo dnf install -y {dnf['packages']}; " + # Cascade d'environnements : le premier qui existe gagne. Un + # environnement absent fait rendre 1 à dnf sans rien installer, + # d'où le « || » plutôt qu'une détection préalable. + "de_ok=0; " + f"for e in {de['dnf_env']}; do " + 'sudo dnf -y group install "$e" && { de_ok=1; break; }; done; ' + '[ "$de_ok" = 1 ] || echo "Aucun environnement graphique dnf ' + "trouve pour " + label + '"; ' + f"sudo dnf install -y {rem['dnf']['packages']}; " "elif command -v pacman >/dev/null 2>&1; then " "pgrep -x pacman >/dev/null 2>&1 " "|| sudo rm -f /var/lib/pacman/db.lck; " - f"sudo pacman -S --needed --noconfirm {pac['groups']} " - f"{pac['packages']}; " + f"sudo pacman -S --needed --noconfirm {de['pacman']} " + f"{rem['pacman']['packages']}; " "else echo 'Gestionnaire de paquets inconnu'; exit 1; fi; " # Le bureau ne sert à rien s'il ne démarre pas tout seul : les # images cloud démarrent en multi-user.target. "sudo systemctl set-default graphical.target || true; " - "sudo systemctl enable gdm >/dev/null 2>&1 || true; " + f"sudo systemctl enable {de['service']} >/dev/null 2>&1 || true; " # xrdp là où il existe ; sur Arch c'est TigerVNC, qui se configure # par utilisateur et n'a pas de service à activer d'office. "if command -v xrdp >/dev/null 2>&1; then " @@ -4453,7 +4475,7 @@ class TODO: "set -e; " + self._qemu_cloud_init_wait() + self._qemu_no_auto_upgrade(prod) - + self._qemu_desktop_remote_cmd() + + self._qemu_desktop_remote_cmd(desktop) ) if not final_cmd: final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}" @@ -4487,7 +4509,7 @@ class TODO: # Le bureau d'abord : il repose sur les dépôts de la distribution, # là où l'installation ERPLibre compile longuement. Un échec ici se # voit donc tôt plutôt qu'après une heure. - + (self._qemu_desktop_remote_cmd() if desktop else "") + + + (self._qemu_desktop_remote_cmd(desktop) if desktop else "") + # Outils d'amorçage (absents des images cloud minimales) : curl, # git, make. Chaque branche RAFRAÎCHIT d'abord les dépôts pour que # la VM soit la plus rapide possible (miroirs à jour / les plus @@ -5039,10 +5061,13 @@ class TODO: ) else: print(f" {t('ERPLibre install:')} {t('no')}") - if spec.get("desktop"): + flavour = spec.get("desktop") + if flavour: + label = (self._QEMU_DESKTOP.get(flavour) or {}).get( + "label", flavour + ) print( - f" {t('VM type:')} " - f"{t('Graphical (server + GNOME desktop)')}" + f" {t('VM type:')} {t('Graphical (server + desktop):')} {label}" ) print(f" {t('SSH key:')} {spec.get('ssh_key') or t('none')}") cfg = ( @@ -5161,17 +5186,10 @@ class TODO: déployable, aussi bien pour la liste granulaire de la CLI que pour la liste à cocher du formulaire TUI.""" flat = [] - # Le catalogue ne propose que ce qui s'installe. La liste des versions - # abandonnées vit dans deploy_qemu.py, qui refuse aussi de les - # déployer : une seule source, donc pas d'écran offrant un choix que - # la couche du dessous rejettera. - supported = getattr(mod, "is_supported", None) for d in distros: versions_map, default_v = mod.DISTROS[d] for v, (_c, _o, ram, disk) in versions_map.items(): for a in self._qemu_arches_for(d, arch): - if supported and not supported(d, v, a): - continue flat.append( { "distro": d, @@ -5371,6 +5389,9 @@ class TODO: "disk_presets": self._QEMU_DISK_PRESETS, "extra_disk_gb": self.ERPLIBRE_EXTRA_DISK_GB, "desktop_disk_gb": self.QEMU_DESKTOP_EXTRA_DISK_GB, + "desktops": [ + (k, v["label"]) for k, v in self._QEMU_DESKTOP.items() + ], "defaults": { "install": True, "add_ssh_config": True, @@ -5641,14 +5662,22 @@ class TODO: print(f" ⚠ {warn}") def _qemu_ask_desktop(self): - """Serveur, ou serveur plus un bureau GNOME. + """Serveur, ou serveur plus un bureau. Renvoie "" ou la saveur. Serveur par défaut : c'est ce que sert une image cloud, et le bureau ajoute une à deux heures d'installation sur une architecture émulée.""" print(f"\n{t('VM type:')}") print(f" [1] {t('Server (no graphical interface)')} *") - print(f" [2] {t('Graphical (server + GNOME desktop)')}") - return input(t("Choice (number, blank = server): ")).strip() == "2" + flavours = list(self._QEMU_DESKTOP) + for i, key in enumerate(flavours, 2): + label = self._QEMU_DESKTOP[key]["label"] + print(f" [{i}] {t('Graphical (server + desktop):')} {label}") + sel = input(t("Choice (number, blank = server): ")).strip() + try: + index = int(sel) - 2 + except ValueError: + return "" + return flavours[index] if 0 <= index < len(flavours) else "" def _qemu_host_timezone(self): """Fuseau de l'hôte. Défini une seule fois, dans deploy_qemu.py, qui @@ -5853,7 +5882,7 @@ class TODO: deployed = list(spec.get("existing") or []) install = spec.get("install") install_branch = install["branch"] if install else None - desktop = bool(spec.get("desktop")) + desktop = spec.get("desktop") or "" ssh_key = spec.get("ssh_key") add_ssh_config = spec["add_ssh_config"] parallelism = spec["parallelism"] diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 0c4cbf1..06289a0 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2035,6 +2035,14 @@ TRANSLATIONS = { "fr": "Graphique (serveur + bureau GNOME)", "en": "Graphical (server + GNOME desktop)", }, + "Graphical (server + desktop):": { + "fr": "Graphique (serveur + bureau) :", + "en": "Graphical (server + desktop):", + }, + "Installing the desktop (long):": { + "fr": "Installation du bureau (long) :", + "en": "Installing the desktop (long):", + }, "Choice (number, blank = server): ": { "fr": "Choix (numero, vide = serveur) : ", "en": "Choice (number, blank = server): ", From 3d6d2395b3d51bcb8c6a7365bbeae5a80b0a0f3e Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 18:55:52 -0400 Subject: [PATCH 12/41] [UPD] tui qemu: the plan shows each VM type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server-or-desktop choice only showed on the left, in the form. The table on the right, the one read before launching, said nothing about it: two visually identical plans could produce different VMs. The Status column, empty until now for a VM to create, therefore carries the type, and the totals line repeats it. An already defined VM keeps its collision message: it is not touched, showing a desktop there would suggest one is about to be installed on it. --- FR --- Le choix serveur ou bureau ne se voyait qu'à gauche, dans le formulaire. Le tableau de droite, celui qu'on relit avant de lancer, n'en disait rien : deux plans identiques à l'écran pouvaient produire des VM différentes. La colonne Statut, vide jusqu'ici pour une VM à créer, porte donc le type, et la ligne de totaux le rappelle. Une VM déjà définie garde son message de collision : elle n'est pas retouchée, lui afficher un bureau laisserait croire qu'on va lui en poser un. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 21 ++++++++++++++++++--- script/todo/todo_i18n.py | 4 ++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 7f75bd8..83658f3 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -608,12 +608,25 @@ def run_deploy_form(ctx, run_app: bool = True): return "" return desktops[index - 1][0] + def _desktop_label(self): + """Libellé court du type de VM, pour le tableau et les totaux.""" + key = self._desktop() + if not key: + return t("server") + label = dict(desktops).get(key, key) + return f"🖥 {label}" + def _render_plan(self): table = self.query_one("#plan", DataTable) table.clear() + # Le type ne concerne QUE les VM réellement créées : une VM déjà + # définie n'est pas retouchée, lui afficher « GNOME » laisserait + # croire qu'on va lui poser un bureau. + kind = self._desktop_label() for r in self.rows: vm = r["vm"] icon = {"new": "", "exists": "⏭ ", "orphan": "❌ "}[r["state"]] + status = kind if r["state"] == "new" else f"{icon}{r['note']}" table.add_row( vm["name"], vm["distro"], @@ -622,7 +635,7 @@ def run_deploy_form(ctx, run_app: bool = True): str(vm["vcpus"]), f"{vm['ram']}Mo", f"{r['disk_gb']}G", - f"{icon}{r['note']}", + status, ) if not self.rows: # Rien de coché : un total à zéro n'apprend rien, on dit @@ -646,7 +659,7 @@ def run_deploy_form(ctx, run_app: bool = True): ) self.query_one("#totals", Static).update( f" {n} {t('VMs')} · {cpus} vCPU · {ram} Mo · ~{disk} G" - f"{warn}{dup_txt}" + f" · {self._desktop_label()}{warn}{dup_txt}" ) # -- réactions aux champs -------------------------------------- # @@ -655,7 +668,9 @@ def run_deploy_form(ctx, run_app: bool = True): self.arch = arches[event.radio_set.pressed_index] self._reload_catalog() elif event.radio_set.id == "f_type": - self._recompute() # le disque annonce inclut GNOME + # Recalcul : le disque annonce inclut le bureau, et la + # colonne Statut affiche le type de VM. + self._recompute() elif event.radio_set.id == "f_profile": index = event.radio_set.pressed_index self.profile = "custom" if index == 4 else str(index + 1) diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 06289a0..4c5ffde 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2035,6 +2035,10 @@ TRANSLATIONS = { "fr": "Graphique (serveur + bureau GNOME)", "en": "Graphical (server + GNOME desktop)", }, + "server": { + "fr": "serveur", + "en": "server", + }, "Graphical (server + desktop):": { "fr": "Graphique (serveur + bureau) :", "en": "Graphical (server + desktop):", From 96464ab0ebd8eaf9f7ad1a506de25bbdcfa1ceb0 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 19:55:09 -0400 Subject: [PATCH 13/41] [ADD] catalogue: Fedora 43 for s390x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fedora does publish an s390x cloud image, just elsewhere: the arch is SECONDARY there, its images live under "fedora-secondary" rather than /pub/fedora/linux/. The catalog comment read "404" -- it was the wrong tree. It brings what nothing else has on this architecture: qpdf 12.2.0, exactly the pikepdf 10 threshold. The qpdf build, half an hour under emulation, disappears. With GCC 15, Python 3.14 and cargo 1.90, none of the s390x workarounds fire there. Only 43 is kept: Fedora builds s390x for the current release only. 41 and 42 return 404 on the master mirror, 44 sits on third-party mirrors only. Hence a positive per-architecture declaration, read by both interfaces -- the catalog never offers what deployment would refuse. --- FR --- Fedora publie bien une image cloud s390x, mais ailleurs : l'architecture est SECONDAIRE chez elle, ses images vivent sous « fedora-secondary » et non sous /pub/fedora/linux/. Le commentaire du catalogue disait « 404 » — c'était la mauvaise arborescence. Elle apporte ce qu'aucune autre n'a sur cette architecture : qpdf 12.2.0, soit exactement le seuil de pikepdf 10. La compilation de qpdf, une demi- heure sous émulation, disparaît. Avec GCC 15, Python 3.14 et cargo 1.90, aucun des contournements s390x ne s'y déclenche. Seule la 43 est retenue : Fedora ne construit s390x que pour la version courante. Les 41 et 42 rendent 404 sur le miroir maître, la 44 n'est que sur certains miroirs tiers. D'où une déclaration positive par architecture, que les deux interfaces relisent — le catalogue ne propose pas ce que le déploiement refuserait. Assisted-by: Claude Opus 5 --- script/qemu/README.base.md | 10 ++++++-- script/qemu/README.fr.md | 5 +++- script/qemu/README.md | 5 +++- script/qemu/deploy_qemu.py | 49 +++++++++++++++++++++++++++++++++++--- script/todo/todo.py | 9 ++++++- 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/script/qemu/README.base.md b/script/qemu/README.base.md index cca9e41..240ac2c 100644 --- a/script/qemu/README.base.md +++ b/script/qemu/README.base.md @@ -179,11 +179,14 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth): |---|---|:-:|:-:|:-:| | ubuntu | `24.04` (default), `25.10`, `26.04` | ✔ | ✔ | ✔ | | debian | `11`, `12` (default), `13` | ✔ | ✔ | — | -| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | — | +| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | | almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | +Fedora builds s390x only for the current release, and on a separate tree +(`fedora-secondary`) — hence the single version there. + Provide an explicit image path as a positional argument to override the automatic download location. @@ -201,11 +204,14 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) : |---|---|:-:|:-:|:-:| | ubuntu | `24.04` (défaut), `25.10`, `26.04` | ✔ | ✔ | ✔ | | debian | `11`, `12` (défaut), `13` | ✔ | ✔ | — | -| fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | — | +| fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule | | almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | +Fedora ne construit s390x que pour la version courante, et sur une +arborescence à part (`fedora-secondary`) — d'où la version unique. + Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. diff --git a/script/qemu/README.fr.md b/script/qemu/README.fr.md index 19c022f..063279b 100644 --- a/script/qemu/README.fr.md +++ b/script/qemu/README.fr.md @@ -98,11 +98,14 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) : |---|---|:-:|:-:|:-:| | ubuntu | `24.04` (défaut), `25.10`, `26.04` | ✔ | ✔ | ✔ | | debian | `11`, `12` (défaut), `13` | ✔ | ✔ | — | -| fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | — | +| fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule | | almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | +Fedora ne construit s390x que pour la version courante, et sur une +arborescence à part (`fedora-secondary`) — d'où la version unique. + Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. diff --git a/script/qemu/README.md b/script/qemu/README.md index a4163a7..0ca3842 100644 --- a/script/qemu/README.md +++ b/script/qemu/README.md @@ -92,11 +92,14 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth): |---|---|:-:|:-:|:-:| | ubuntu | `24.04` (default), `25.10`, `26.04` | ✔ | ✔ | ✔ | | debian | `11`, `12` (default), `13` | ✔ | ✔ | — | -| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | — | +| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | | almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | +Fedora builds s390x only for the current release, and on a separate tree +(`fedora-secondary`) — hence the single version there. + Provide an explicit image path as a positional argument to override the automatic download location. diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index 21c9a72..267c0ef 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -146,7 +146,27 @@ NON_X86_ARCHES: tuple[str, ...] = ("arm64", "s390x") # - s390x (IBM Z) : Ubuntu seulement (Debian/Fedora : 404 ; Arch : x86/arm). # - arm64/aarch64 : Ubuntu, Debian, Fedora (Arch : pas d'image cloud officielle # aarch64 sur geo.mirror.pkgbuild.com). -S390X_DISTROS: tuple[str, ...] = ("ubuntu", "almalinux", "rocky") +S390X_DISTROS: tuple[str, ...] = ("ubuntu", "almalinux", "rocky", "fedora") + +# Une distro peut ne publier qu'une PARTIE de ses versions sur une +# architecture. Déclaration POSITIVE : hors de cette table, toutes les versions +# du catalogue sont réputées disponibles. +# +# Fedora ne construit s390x que pour la version courante, et sur une +# arborescence à part (« fedora-secondary ») : la 41 et la 42 y sont retirées +# (404 sur le miroir maître), la 44 n'est pour l'instant que sur certains +# miroirs tiers. Seule la 43 est servie par dl.fedoraproject.org — vérifié. +ARCH_ONLY_VERSIONS: dict[str, dict[str, tuple[str, ...]]] = { + "s390x": {"fedora": ("43",)}, +} + + +def arch_versions(distro: str, arch: str, versions) -> list[str]: + """Versions de `distro` réellement publiées pour `arch`.""" + only = ARCH_ONLY_VERSIONS.get(arch, {}).get(distro) + return [v for v in versions if only is None or v in only] + + ARM64_DISTROS: tuple[str, ...] = ( "ubuntu", "debian", @@ -197,6 +217,15 @@ FEDORA_BASE = "https://download.fedoraproject.org/pub/fedora/linux/releases" # redirecteur envoie sur un miroir incomplet (fréquent en déploiement # PARALLÈLE : chaque requête peut tomber sur un miroir différent/désynchronisé). FEDORA_BASE_MASTER = "https://dl.fedoraproject.org/pub/fedora/linux/releases" +# s390x est une architecture SECONDAIRE chez Fedora : ses images ne sont PAS +# sous /pub/fedora/linux/ mais sous /pub/fedora-secondary/, et seule la +# version courante y est construite. +FEDORA_SECONDARY = ( + "https://download.fedoraproject.org/pub/fedora-secondary/releases" +) +FEDORA_SECONDARY_MASTER = ( + "https://dl.fedoraproject.org/pub/fedora-secondary/releases" +) # Répertoire de cache par défaut des images cloud (cohérent avec --disk-dir / # --seed-dir). L'écriture y nécessite root : le déploiement tourne de toute @@ -275,12 +304,18 @@ def resolve_fedora_url(version: str, arch: str, dry_run: bool) -> str: pattern = re.compile( rf"Fedora-Cloud-Base-Generic-{version}-[0-9.]+\.{a}\.qcow2" ) + # Architecture secondaire (s390x, ppc64le) : autre arborescence. + primary, master = ( + (FEDORA_SECONDARY, FEDORA_SECONDARY_MASTER) + if arch == "s390x" + else (FEDORA_BASE, FEDORA_BASE_MASTER) + ) if dry_run: - index = f"{FEDORA_BASE}/{version}/Cloud/{a}/images/" + index = f"{primary}/{version}/Cloud/{a}/images/" return index + f"Fedora-Cloud-Base-Generic-{version}-.{a}.qcow2" # On essaie le redirecteur (2 fois : chaque requête peut viser un miroir # différent) puis le serveur maître (toujours complet). - bases = [FEDORA_BASE, FEDORA_BASE, FEDORA_BASE_MASTER] + bases = [primary, primary, master] last_err = "" for base in bases: index = f"{base}/{version}/Cloud/{a}/images/" @@ -2173,6 +2208,14 @@ def main() -> None: f"Architecture {args.arch} indisponible pour {args.distro!r} : " f"images cloud publiées seulement pour {', '.join(supported)}." ) + # Toutes les versions d'une distro ne sont pas publiées sur toutes les + # architectures : le dire ici plutôt que d'échouer au téléchargement. + ok_versions = arch_versions(args.distro, args.arch, versions) + if args.version not in ok_versions: + sys.exit( + f"{args.distro} {args.version} n'est pas publié en {args.arch}.\n" + f" Versions disponibles : {', '.join(ok_versions) or 'aucune'}." + ) code, default_osinfo, min_ram, min_disk = versions[args.version] if args.codename: code = args.codename diff --git a/script/todo/todo.py b/script/todo/todo.py index b9bf152..c3709f3 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1074,7 +1074,7 @@ class TODO: # Distros publiant des images cloud par architecture (cohérent avec # S390X_DISTROS / ARM64_DISTROS de deploy_qemu.py). amd64 : toutes. - _QEMU_S390X_DISTROS = ("ubuntu", "almalinux", "rocky") + _QEMU_S390X_DISTROS = ("ubuntu", "almalinux", "rocky", "fedora") _QEMU_ARM64_DISTROS = ( "ubuntu", "debian", @@ -5186,10 +5186,17 @@ class TODO: déployable, aussi bien pour la liste granulaire de la CLI que pour la liste à cocher du formulaire TUI.""" flat = [] + # Une distro peut ne publier qu'une partie de ses versions sur une + # architecture (Fedora ne construit que la courante en s390x). La + # table vit dans deploy_qemu.py, qui refuse aussi ces combinaisons : + # une seule source, donc aucun écran n'offre un choix rejeté ensuite. + only = getattr(mod, "arch_versions", None) for d in distros: versions_map, default_v = mod.DISTROS[d] for v, (_c, _o, ram, disk) in versions_map.items(): for a in self._qemu_arches_for(d, arch): + if only and v not in only(d, a, versions_map): + continue flat.append( { "distro": d, From 7120732c255ac9b6398e2acc0fbd6033f48da9e3 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 20:38:12 -0400 Subject: [PATCH 14/41] [ADD] install: openSUSE support, mirrors and packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under s390x emulation never runs there. It is also the only family foreign to both RHEL and Debian, and it brings zypper, absent from the repository: wired into the install_dev.sh dispatch, a dependency script, the remote bootstrap and the desktop block. Four SUSE traps, all met on real machines. Global options precede the subcommand, and misplaced ones abort the call; without "--auto-agree-with-licenses" zypper waits for an answer nobody gives in a detached install. A compat package that PROVIDES another under a different name makes zypper raise a conflict and drop the whole batch, so only what nothing already provides is requested. pkg-config no longer exists as an RPM, only as a capability. And a single mirror, unreachable one day, sent everyone back to Europe -- three are probed in order now. CentOS Stream is dropped again: it served as a canary but 10 duplicates what Alma and Rocky already cover. --- FR --- Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un script de dépendances, l'amorçage distant et le bloc bureau. Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options globales précèdent la sous-commande, mal placées elles arrêtent l'appel ; sans « --auto-agree-with-licenses » zypper attend une réponse que personne ne donne dans une installation détachée. Un paquet compat qui FOURNIT un autre sous un nom différent fait lever un conflit à zypper, qui abandonne le lot entier : on ne demande donc que ce que rien ne fournit déjà. pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir unique, injoignable un jour, renvoyait tout le monde en Europe — trois sont désormais sondés dans l'ordre. CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et Rocky couvrent déjà. Assisted-by: Claude Opus 5 --- .claude/rules/08-deployment.md | 3 +- CHANGELOG.base.md | 6 +- CHANGELOG.fr.md | 3 +- CHANGELOG.md | 3 +- script/install/install_dev.sh | 26 +- script/install/install_suse_dependency.sh | 337 ++++++++++++++++++++++ script/qemu/README.base.md | 10 + script/qemu/README.fr.md | 5 + script/qemu/README.md | 5 + script/qemu/deploy_qemu.py | 57 +++- script/todo/todo.py | 80 ++++- script/todo/todo_i18n.py | 4 + 12 files changed, 522 insertions(+), 17 deletions(-) create mode 100755 script/install/install_suse_dependency.sh diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md index cd8beb7..bd6c86a 100644 --- a/.claude/rules/08-deployment.md +++ b/.claude/rules/08-deployment.md @@ -7,7 +7,8 @@ - **DNS** : `script/deployment/update_dns_cloudflare.py` Plateformes supportées : Ubuntu 24.04 / 25.10 / 26.04, Linux Mint 22.3, -Debian 12, AlmaLinux 9+, Rocky Linux 9+, Arch Linux, macOS (pyenv), +Debian 12, AlmaLinux 9+, Rocky Linux 9+, openSUSE Tumbleweed, Arch Linux, +macOS (pyenv), Windows (WSL/Docker). Ubuntu 20.04 et 22.04 sont abandonnées : pikepdf exige qpdf >= 12.2, compilé diff --git a/CHANGELOG.base.md b/CHANGELOG.base.md index 56963b3..77386e5 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -58,7 +58,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Support Selenium grid from selenium_lib.py - Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, - AlmaLinux, Rocky Linux, Arch) on amd64, arm64 and s390x, with a menu to list, + AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) on amd64, + arm64 and s390x, with a menu to list, test, resize, delete and clean up - Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch @@ -111,7 +112,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Support de la grille Selenium depuis selenium_lib.py - Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, - Debian, Fedora, AlmaLinux, Rocky Linux, Arch) en amd64, arm64 et s390x, avec + Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, + Arch) en amd64, arm64 et s390x, avec un menu pour lister, tester, redimensionner, supprimer et nettoyer - VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 9908a6e..040fc6c 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -32,7 +32,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Support de la grille Selenium depuis selenium_lib.py - Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, - Debian, Fedora, AlmaLinux, Rocky Linux, Arch) en amd64, arm64 et s390x, avec + Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, + Arch) en amd64, arm64 et s390x, avec un menu pour lister, tester, redimensionner, supprimer et nettoyer - VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch diff --git a/CHANGELOG.md b/CHANGELOG.md index 98cdeb8..7d4b996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,8 @@ Recreating the virtual environment, use installation guide from tool `make`. - Support Selenium grid from selenium_lib.py - Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor - Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, - AlmaLinux, Rocky Linux, Arch) on amd64, arm64 and s390x, with a menu to list, + AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) on amd64, + arm64 and s390x, with a menu to list, test, resize, delete and clean up - Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch diff --git a/script/install/install_dev.sh b/script/install/install_dev.sh index 9274230..3e6f8f7 100755 --- a/script/install/install_dev.sh +++ b/script/install/install_dev.sh @@ -1,6 +1,16 @@ #!/usr/bin/env bash -if [[ "${OSTYPE}" == "linux-gnu" ]]; then +# « $OSTYPE » ne decrit PAS le systeme : bash le fige au triplet avec lequel il +# a ete compile. openSUSE batit bash en « x86_64-suse-linux » et le renseigne +# donc a « linux », la ou Debian et Fedora donnent « linux-gnu ». Le test +# echouait, sans else, donc sans une ligne de sortie ni code d'erreur : AUCUNE +# dependance systeme n'etait posee, install_system() rendait 0, et +# l'installation s'arretait une heure plus tard sur « [Errno 2] ... 'cc' ». +# « uname -s » interroge le noyau et ne depend pas de la facon dont bash a ete +# bati. install_dev_extra_ubuntu.sh garde OSTYPE : il ne vise qu'Ubuntu. +OS_KERNEL="$(uname -s)" + +if [[ "${OS_KERNEL}" == "Linux" ]]; then source /etc/os-release if [[ "${ID}" == "ubuntu" ]]; then # 20.04 et 22.04 retirees : leur chaine d'outils ne suffit plus a ERPLibre @@ -24,6 +34,10 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then ./script/install/install_debian_dependency.sh elif [[ "${ID}" == "arch" ]]; then ./script/install/install_arch_linux.sh + elif [[ "${ID}" == opensuse* || "${ID_LIKE}" == *"suse"* ]]; then + # openSUSE Tumbleweed / Leap, SLES : gestionnaire zypper. + echo "\n---- SUSE installation process started ----" + ./script/install/install_suse_dependency.sh elif [[ "${ID}" == "fedora" || "${ID_LIKE}" == *"fedora"* || "${ID_LIKE}" == *"rhel"* ]]; then # AlmaLinux et Rocky declarent ID_LIKE="rhel centos fedora" : elles passent # donc par ce meme script, qui aiguille dnf. @@ -31,9 +45,15 @@ if [[ "${OSTYPE}" == "linux-gnu" ]]; then ./script/install/install_fedora_dependency.sh else ./script/install/install_debian_dependency.sh - echo "Your Linux system is not supported, only support Ubuntu 24.04, 25.10, 26.04, Linux Mint 22.3, Debian, Fedora, AlmaLinux, Rocky Linux, Arch." + echo "Your Linux system is not supported, only support Ubuntu 24.04, 25.10, 26.04, Linux Mint 22.3, Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE, Arch." fi -elif [[ "${OSTYPE}" == "darwin"* ]]; then +elif [[ "${OS_KERNEL}" == "Darwin" ]]; then echo "\n---- Darwin installation process started ----" ./script/install/install_OSX_dependency.sh +else + # Un aiguillage muet qui rend 0 est le pire des cas : l'installation continue + # sur un systeme depourvu de tout. Meme sans savoir quoi poser, on le dit et + # on arrete. + echo "Systeme non reconnu (uname -s = ${OS_KERNEL}) : aucune dependance installee." + exit 1 fi diff --git a/script/install/install_suse_dependency.sh b/script/install/install_suse_dependency.sh new file mode 100755 index 0000000..9abddc2 --- /dev/null +++ b/script/install/install_suse_dependency.sh @@ -0,0 +1,337 @@ +#!/usr/bin/env bash +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Dépendances système ERPLibre pour la famille SUSE (zypper). Équivalent SUSE +# de install_debian_dependency.sh et install_fedora_dependency.sh. +# +# Visé : openSUSE Tumbleweed, la seule distribution du catalogue dont qpdf +# (12.3.2) dépasse déjà le seuil de pikepdf 10 — la compilation de qpdf, une +# demi-heure sous émulation s390x, ne s'y déclenche pas. + +. ./env_var.sh + +EL_USER=${USER} + +# « --non-interactive » est une option GLOBALE de zypper, elle précède la +# sous-commande. Les deux autres appartiennent à « install » et doivent la +# SUIVRE : placées avant, zypper répond « The flag +# --auto-agree-with-licenses is not known » et s'arrête. +# +# On demande à zypper quelles options il accepte, plutôt que de le supposer : +# la famille SUSE n'est pas testée ici, et un drapeau inconnu fait échouer la +# commande entière au lieu du seul paquet visé. +# --auto-agree-with-licenses : sinon zypper attend l'acceptation d'une +# licence, réponse que personne ne donnera dans une installation détachée. +# --ignore-unknown : saute un nom de paquet absent au lieu de refuser tout +# le lot, l'équivalent de « --skip-unavailable » côté dnf. +ZYP="sudo zypper --non-interactive" +ZYP_HELP="$(zypper install --help 2>&1 || true)" +ZYP_LIC="" +ZYP_IGN="" +case "${ZYP_HELP}" in + *--auto-agree-with-licenses*) ZYP_LIC="--auto-agree-with-licenses" ;; +esac +case "${ZYP_HELP}" in + *--ignore-unknown*) ZYP_IGN="--ignore-unknown" ;; +esac +echo "zypper : options retenues = ${ZYP_LIC} ${ZYP_IGN}" +# Lot obligatoire : un paquet manquant doit se voir. +ZYP_IN="${ZYP} install ${ZYP_LIC}" +# Lot best-effort : un nom absent est sauté. +ZYP_SOFT="${ZYP_IN} ${ZYP_IGN}" + +# openSUSE remplace des paquets par des variantes « compat » qui les FOURNISSENT +# sans en porter le nom : sur Tumbleweed, zlib-ng-compat-devel fournit +# zlib-devel et se trouve posé d'office. Demander « zlib-devel » par son nom +# force alors un échange, zypper soulève un conflit — et --non-interactive n'a +# aucune solution par défaut : il énumère les choix, puis ABANDONNE le lot +# entier. C'est ce qui a arrêté l'installation sur amd64, en emportant les huit +# autres dépendances de pyenv qui, elles, ne posaient aucun problème. +# « --ignore-unknown » ne couvre que les noms absents, jamais les conflits. +# +# On ne demande donc que ce que rien ne fournit déjà. rpm interroge les +# « Provides », pas seulement les noms : c'est la seule autorité sur la +# question. Effet de bord bienvenu, une relance ne redemande plus rien. +zyp_filter() { + local p + for p in "$@"; do + rpm -q --whatprovides "${p}" > /dev/null 2>&1 || printf '%s\n' "${p}" + done +} + +# Un telechargement tronque par le miroir fait echouer le LOT ENTIER, et +# zypper ne reessaie pas. Vecu : « libLLVM22 ... OpenSSL SSL_read: unexpected +# eof while reading » a emporte « rust cargo », et l'absence de cargo n'est +# apparue qu'une heure plus tard, dans la compilation de bcrypt. Une seconde +# tentative suffit dans la quasi-totalite des cas. +zyp_retry() { + local cmd=("$@") + "${cmd[@]}" && return 0 + echo " zypper a echoue (reseau ?), seconde tentative..." >&2 + "${cmd[@]}" +} + +zyp_in() { + local todo + mapfile -t todo < <(zyp_filter "$@") + if [ "${#todo[@]}" -eq 0 ]; then + echo " deja fourni : $*" + return 0 + fi + zyp_retry ${ZYP_IN} "${todo[@]}" +} + +zyp_soft() { + local todo + mapfile -t todo < <(zyp_filter "$@") + if [ "${#todo[@]}" -eq 0 ]; then + echo " deja fourni : $*" + return 0 + fi + zyp_retry ${ZYP_SOFT} "${todo[@]}" +} + +#-------------------------------------------------- +# Miroir des dépôts +#-------------------------------------------------- +# Le redirecteur officiel d'openSUSE n'est PAS géographique : mesuré depuis +# Montréal sur les métadonnées oss s390x (15 Mo), download.opensuse.org met +# 23,8 s — il sert depuis l'Europe — contre 2,7 s pour mirrors.rit.edu. +# +# Chaque miroir est SONDÉ sur le chemin de l'architecture courante : tous ne +# répliquent pas les architectures secondaires, et rit.edu sert justement +# zsystems mais pas x86_64. Aucun sondage concluant : on garde les dépôts de +# l'image, donc le comportement d'avant. +# Plusieurs miroirs, sondés dans l'ordre sur le chemin exact du produit et +# de l'architecture : aucun ne réplique tout. Relevé le 2026-08-12 — +# csclub sert Leap mais pas Tumbleweed, leaseweb sert Tumbleweed x86_64 +# mais pas les ports zsystems, et rit.edu — seule entrée jusqu'ici — était +# injoignable ce jour-là, ce qui renvoyait tout le monde en Europe. +ZYP_MIRRORS="https://mirror.csclub.uwaterloo.ca/opensuse https://mirrors.rit.edu/opensuse https://mirror.us.leaseweb.net/opensuse" +# Leap et Tumbleweed sont deux PRODUITS, pas deux versions du même : arbres de +# dépôts différents, et surtout une mise à jour de nature différente (plus bas). +SUSE_ID="$([ -r /etc/os-release ] && . /etc/os-release && echo "${ID}")" +SUSE_VER="$([ -r /etc/os-release ] && . /etc/os-release && echo "${VERSION_ID}")" +case "${SUSE_ID}" in + *tumbleweed*) SUSE_ROLLING=1 ;; + *) SUSE_ROLLING=0 ;; +esac +if [ "${SUSE_ROLLING}" = 1 ]; then + # Tumbleweed isole les architectures secondaires sous /ports/. + zp=tumbleweed + [ "$(uname -m)" = s390x ] && zp=ports/zsystems/tumbleweed +else + # Leap 16 unifie tout : un seul arbre, avec s390x dedans. Les chemins + # /ports/ équivalents rendent 404 — relevé, pas déduit. + zp="distribution/leap/${SUSE_VER}" +fi +for zm in ${ZYP_MIRRORS}; do + if curl -fsS --max-time 20 -o /dev/null \ + "${zm}/${zp}/repo/oss/repodata/repomd.xml"; then + sudo sed -i "s|https\?://download\.opensuse\.org|${zm}|g" \ + /etc/zypp/repos.d/*.repo 2> /dev/null || true + echo "miroir openSUSE : ${zm}" + break + fi +done + +echo -e "\n---- Rafraichissement des depots ----" +# Tumbleweed est ROLLING : installer sans rafraîchir mène à des paquets +# introuvables (l'index local pointe des versions déjà retirées du miroir). +sudo zypper --non-interactive refresh || true + +# Mise à jour COMPLÈTE, obligatoire pour la même raison qu'Arch : une rolling +# release ne supporte PAS les mises à jour partielles. L'image cloud est un +# instantané figé dont les dépôts ont avancé — vécu sur s390x, git 2.54 +# réclamait un perl-Git bâti contre un perl-base plus ancien que celui de +# l'image. zypper proposait alors trois solutions et ATTENDAIT un choix ; +# « --non-interactive » prend le défaut, « c » = annuler, et tout s'arrêtait. +echo -e "\n---- Mise a jour complete (dup) ----" +${ZYP} dup ${ZYP_LIC} --allow-vendor-change || true + +#-------------------------------------------------- +# Outils de compilation (build Python via pyenv, extensions Python) +#-------------------------------------------------- +echo -e "\n---- Outils de developpement ----" +# Les compilateurs sont installés EXPLICITEMENT, jamais par le seul motif : +# un « pattern » absent ou incomplet laisserait le système sans g++, et +# l'échec n'apparaîtrait qu'à la compilation de numpy — la leçon d'EL, où le +# groupe « development-tools » n'existe pas et où le repli ne partait jamais. +# Les noms non versionnés existent bien dans Tumbleweed (relevé dans le +# primary.xml du dépôt oss) : « gcc » y est un méta-paquet qui tire gcc15. +zyp_in gcc gcc-c++ make automake patch +retVal=$? +if [[ ${retVal} -ne 0 ]]; then + echo "zypper install compilers error." + exit 1 +fi +# Le motif ensuite, en complément et sans jamais bloquer. +${ZYP_IN} -t pattern devel_basis > /dev/null 2>&1 || true + +# Python appelle son compilateur par le nom gravé dans sysconfig, souvent +# « cc ». Sur openSUSE ce lien n'est garanti par aucune déclaration de +# paquet — vécu sur amd64 : « cc -pthread … error: [Errno 2] No such file or +# directory » à la compilation de python-ldap, alors que gcc était installé. +if ! command -v cc > /dev/null 2>&1 && command -v gcc > /dev/null 2>&1; then + sudo ln -sf "$(command -v gcc)" /usr/local/bin/cc + echo "cc -> $(command -v gcc)" +fi + +#-------------------------------------------------- +# PostgreSQL +#-------------------------------------------------- +echo -e "\n---- Install PostgreSQL Server ----" +zyp_in postgresql-server postgresql-contrib postgresql-server-devel +retVal=$? +if [[ ${retVal} -ne 0 ]]; then + echo "zypper install postgresql installation error." + exit 1 +fi +# openSUSE n'initialise pas le cluster : le service le fait au 1er démarrage, +# mais seulement si le répertoire de données est vide. +sudo systemctl enable --now postgresql 2> /dev/null || true +# PostGIS : absent des dépôts Tumbleweed s390x. Optionnel, ne bloque pas. +zyp_soft postgis || echo "PostGIS non installe (optionnel)." + +echo -e "\n---- Creating the ERPLibre PostgreSQL User ----" +sudo su - postgres -c "createuser -s ${EL_USER}" 2> /dev/null || true + +#-------------------------------------------------- +# Dépendances de build (extensions Python, Odoo) +#-------------------------------------------------- +echo -e "\n--- Installing suse dependency --" +# git-daemon : comme sur Fedora, « git daemon » n'est pas dans le paquet git de +# base. ERPLibre sert son manifeste par git://127.0.0.1:9418/ pendant +# « repo sync » — sans lui, « Connection refused » et synchro impossible. +# Rust n'est nécessaire QUE là où les roues manquent : bcrypt et cryptography +# portent une extension Rust, et sur amd64 leurs roues masquent le besoin. Sur +# s390x tout compile, et bcrypt s'arrête sur « error: can't find Rust +# compiler ». Tumbleweed livre 1.94, bien au-dessus du 1.78 qu'exige le +# Cargo.lock v4 de cryptography. On évite ainsi ~200 Mo inutiles sur amd64. +if [ "$(uname -m)" = "s390x" ]; then + zyp_soft rust cargo + if ! command -v cargo > /dev/null 2>&1; then + # FATAL, et non plus un simple avertissement : sur s390x, bcrypt et + # cryptography n'ont aucune roue et portent une extension Rust. Sans + # cargo l'installation est perdue d'avance — mais elle ne le montrait + # qu'une heure plus tard, au fond d'une trace pip. Mieux vaut s'arreter + # ici, ou la cause est encore lisible. + echo "cargo introuvable apres deux tentatives." >&2 + echo " bcrypt et cryptography ne peuvent pas se compiler sans lui." >&2 + echo " Verifiez le miroir zypper, puis relancez." >&2 + exit 1 + fi + # Sans roue s390x, TOUT se compile contre les en-têtes de la distribution. + # Cette liste reprend donc celles d'apt et de dnf, aux noms d'openSUSE près + # — relevés dans l'index oss de zsystems, jamais devinés. La tenir alignée + # ici plutôt que d'attendre : trois fois de suite, le même manque a été + # découvert d'abord par Debian, puis retrouvé une distribution à la fois. + # tbb-devel manifold3d refuse de configurer sans lui, et le dit dans un + # message de CMake, pas de compilateur : « Parallel mode + # enabled, but tbb was not found ». + # clang-devel pymupdf charge libclang.so par ctypes (lien juste après). + # cmake, pkgconf-pkg-config : nécessaires à el_qpdf_ensure, et ils + # n'arrivent que plus bas dans le script. + # + # « pkgconf-pkg-config » et non « pkg-config » : aucun RPM ne porte ce + # dernier nom dans Tumbleweed, il n'existe plus que comme capacité fournie + # par le premier. La capacité résout aujourd'hui, le nom réel ne dépend + # de rien. + # + # GeographicLib est absent : openSUSE n'empaquette que le binding Python, + # pas la bibliothèque C++. Rien à poser, contrairement à apt et dnf. + zyp_soft qpdf-devel libjpeg8-devel cmake pkgconf-pkg-config \ + tbb-devel geos-devel proj-devel krb5-devel ninja clang-devel llvm-devel + # Tumbleweed livre qpdf 12.3.2 : l'appel ne fait que le constater. Il est là + # pour que les trois familles suivent la même règle. + el_qpdf_ensure + # pymupdf charge « libclang.so » par son nom nu, via ctypes ; le paquet ne + # livre qu'un nom versionné. Il ne manque que le lien — même correctif que + # côté apt et dnf. + for d in /usr/lib64 /usr/lib; do + if [ -d "${d}" ] && [ ! -e "${d}/libclang.so" ]; then + so="$(ls -1 "${d}"/libclang.so.* 2> /dev/null | sort -V | tail -1)" + if [ -n "${so}" ]; then + sudo ln -s "${so}" "${d}/libclang.so" && sudo ldconfig + echo "libclang.so -> ${so}" + fi + fi + done +fi + +zyp_soft \ + git git-daemon wget libxslt-devel libzip-devel openldap2-devel \ + cyrus-sasl-devel libffi-devel libbz2-devel gnu_parallel swig cmake \ + portaudio-devel cups-devel xmlsec1 xmlsec1-openssl-devel \ + libmariadb-devel freetds-devel +retVal=$? +if [[ ${retVal} -ne 0 ]]; then + echo "zypper suse tool installation error." + exit 1 +fi + +# Dépendances de build pour pyenv (compilation de CPython) — CRITIQUE. +echo -e "\n---- Dependances pyenv (compilation Python) ----" +zyp_in \ + make gcc zlib-devel libbz2-devel readline-devel sqlite3-devel \ + libopenssl-devel tk-devel libffi-devel xz-devel patch findutils +retVal=$? +if [[ ${retVal} -ne 0 ]]; then + echo "zypper pyenv dependencies installation error." + exit 1 +fi + +# Dépendances selenium / bindings. +zyp_soft \ + cairo-devel python3-devel pkgconf-pkg-config gobject-introspection-devel \ + libXt-devel || echo "Dependances selenium partielles (optionnel)." + +#-------------------------------------------------- +# Node.js + npm (rtlcss, less) +#-------------------------------------------------- +echo -e "\n---- Installing nodeJS NPM and rtlcss ----" +# openSUSE versionne les paquets node (nodejs22, nodejs24…) et ne fournit pas +# toujours de méta-paquet « nodejs ». On prend le plus récent disponible. +NODE_OK=0 +for pkg in nodejs24 nodejs22 nodejs20 nodejs; do + if ${ZYP_IN} "${pkg}" "${pkg/nodejs/npm}" 2> /dev/null; then + NODE_OK=1 + echo "node fourni par ${pkg}" + break + fi +done +if [[ ${NODE_OK} -ne 1 ]] || ! command -v npm > /dev/null 2>&1; then + echo "zypper nodejs installation error." + exit 1 +fi +sudo npm install -g rtlcss less || echo "npm rtlcss/less: erreur (optionnel)." + +echo -e "\n---- Test tool ----" +npm install || echo "npm install (prettier/plugin-xml): erreur (optionnel)." +sudo ln -fs /usr/local/bin/lessc /usr/bin/lessc 2> /dev/null || true + +#-------------------------------------------------- +# nginx (optionnel) +#-------------------------------------------------- +if [ "${EL_INSTALL_NGINX}" = "True" ]; then + echo -e "\n---- Installing nginx ----" + zyp_soft nginx || echo "nginx: erreur (optionnel)." +fi + +#-------------------------------------------------- +# wkhtmltopdf (optionnel) +#-------------------------------------------------- +if [ "${EL_INSTALL_WKHTMLTOPDF}" = "True" ]; then + if ! command -v wkhtmltopdf > /dev/null 2>&1; then + echo -e "\n---- Installing wkhtml (best-effort) ----" + # wkhtmltopdf ne publie AUCUN paquet openSUSE. Le dépôt de la distribution + # fournit « wkhtmltopdf » quand il existe pour l'architecture ; sinon on + # renonce, Odoo sait imprimer sans lui (rendu dégradé des PDF). + zyp_soft wkhtmltopdf || echo "wkhtmltopdf non installe (optionnel)." + else + echo -e "\n---- Already installed wkhtml ----" + fi +fi + +echo -e "\n---- SUSE dependency installation done ----" diff --git a/script/qemu/README.base.md b/script/qemu/README.base.md index 240ac2c..19fef19 100644 --- a/script/qemu/README.base.md +++ b/script/qemu/README.base.md @@ -182,11 +182,16 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth): | fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | | almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | +| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora builds s390x only for the current release, and on a separate tree (`fedora-secondary`) — hence the single version there. +`opensuse` is openSUSE Tumbleweed, the only entry whose qpdf (12.3.2) already +clears the pikepdf threshold: the half-hour qpdf build never runs there, which +matters under s390x emulation. + Provide an explicit image path as a positional argument to override the automatic download location. @@ -207,11 +212,16 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) : | fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule | | almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | +| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora ne construit s390x que pour la version courante, et sur une arborescence à part (`fedora-secondary`) — d'où la version unique. +`opensuse` désigne openSUSE Tumbleweed, seule entrée dont qpdf (12.3.2) +dépasse déjà le seuil de pikepdf : la compilation de qpdf, une demi-heure, ne +s'y déclenche jamais — ce qui compte sous émulation s390x. + Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. diff --git a/script/qemu/README.fr.md b/script/qemu/README.fr.md index 063279b..52c68ae 100644 --- a/script/qemu/README.fr.md +++ b/script/qemu/README.fr.md @@ -101,11 +101,16 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) : | fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule | | almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | +| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora ne construit s390x que pour la version courante, et sur une arborescence à part (`fedora-secondary`) — d'où la version unique. +`opensuse` désigne openSUSE Tumbleweed, seule entrée dont qpdf (12.3.2) +dépasse déjà le seuil de pikepdf : la compilation de qpdf, une demi-heure, ne +s'y déclenche jamais — ce qui compte sous émulation s390x. + Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. diff --git a/script/qemu/README.md b/script/qemu/README.md index 0ca3842..cda760f 100644 --- a/script/qemu/README.md +++ b/script/qemu/README.md @@ -95,11 +95,16 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth): | fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | | almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | +| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora builds s390x only for the current release, and on a separate tree (`fedora-secondary`) — hence the single version there. +`opensuse` is openSUSE Tumbleweed, the only entry whose qpdf (12.3.2) already +clears the pikepdf threshold: the half-hour qpdf build never runs there, which +matters under s390x emulation. + Provide an explicit image path as a positional argument to override the automatic download location. diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index 267c0ef..c9da563 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -113,6 +113,12 @@ ROCKY_VERSIONS: dict[str, tuple[str, str, int, str]] = { "9": ("9", "rocky9", 2048, "20G"), "10": ("10", "rocky10", 2048, "20G"), } +# openSUSE Tumbleweed : rolling, donc une seule « version ». C'est la seule +# distribution du catalogue dont qpdf (12.3.2) dépasse déjà le seuil de +# pikepdf 10 — aucune compilation de qpdf, ce qui change tout sous émulation. +OPENSUSE_VERSIONS: dict[str, tuple[str, str, int, str]] = { + "tumbleweed": ("tumbleweed", "opensusetumbleweed", 2048, "20G"), +} # Arch est en rolling release : une seule « version » (latest). ARCH_VERSIONS: dict[str, tuple[str, str, int, str]] = { "latest": ("latest", "archlinux", 1024, "20G"), @@ -125,6 +131,7 @@ DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = { "fedora": (FEDORA_VERSIONS, "42"), "almalinux": (ALMALINUX_VERSIONS, "9"), "rocky": (ROCKY_VERSIONS, "10"), + "opensuse": (OPENSUSE_VERSIONS, "tumbleweed"), "arch": (ARCH_VERSIONS, "latest"), } @@ -134,6 +141,7 @@ ARCH_ALIASES: dict[str, dict[str, str]] = { "fedora": {"amd64": "x86_64", "arm64": "aarch64"}, "almalinux": {"amd64": "x86_64", "arm64": "aarch64"}, "rocky": {"amd64": "x86_64", "arm64": "aarch64"}, + "opensuse": {"amd64": "x86_64", "arm64": "aarch64"}, "arch": {"amd64": "x86_64", "arm64": "aarch64"}, } @@ -146,7 +154,13 @@ NON_X86_ARCHES: tuple[str, ...] = ("arm64", "s390x") # - s390x (IBM Z) : Ubuntu seulement (Debian/Fedora : 404 ; Arch : x86/arm). # - arm64/aarch64 : Ubuntu, Debian, Fedora (Arch : pas d'image cloud officielle # aarch64 sur geo.mirror.pkgbuild.com). -S390X_DISTROS: tuple[str, ...] = ("ubuntu", "almalinux", "rocky", "fedora") +S390X_DISTROS: tuple[str, ...] = ( + "ubuntu", + "almalinux", + "rocky", + "fedora", + "opensuse", +) # Une distro peut ne publier qu'une PARTIE de ses versions sur une # architecture. Déclaration POSITIVE : hors de cette table, toutes les versions @@ -173,6 +187,7 @@ ARM64_DISTROS: tuple[str, ...] = ( "fedora", "almalinux", "rocky", + "opensuse", ) @@ -211,6 +226,7 @@ DEBIAN_CLOUD_BASES: tuple[str, ...] = ( "https://laotzu.ftp.acc.umu.se/cdimage/cloud", ) ALMALINUX_CLOUD_BASE = "https://repo.almalinux.org/almalinux" +OPENSUSE_BASE = "https://download.opensuse.org" ROCKY_CLOUD_BASE = "https://dl.rockylinux.org/pub/rocky" FEDORA_BASE = "https://download.fedoraproject.org/pub/fedora/linux/releases" # Serveur MAÎTRE (pas de redirection MirrorManager) : repli fiable quand le @@ -287,6 +303,18 @@ def image_candidates( f"{ROCKY_CLOUD_BASE}/{version}/images/{a}/" f"Rocky-{version}-GenericCloud.latest.{a}.qcow2" ] + if distro == "opensuse": + # Les architectures secondaires vivent sous /ports/, et zsystems DOUBLE + # l'architecture dans le nom du fichier — irrégularité vérifiée dans + # l'index, pas déduite : « .s390x-s390x-Cloud » contre « .x86_64-Cloud ». + port = {"s390x": "ports/zsystems/", "arm64": "ports/aarch64/"}.get( + arch, "" + ) + tag = f"{a}-{a}" if arch == "s390x" else a + return [ + f"{OPENSUSE_BASE}/{port}tumbleweed/appliances/" + f"openSUSE-Tumbleweed-Minimal-VM.{tag}-Cloud.qcow2" + ] if distro == "arch": # Rolling release : image « latest » officielle (cloud-init inclus). return [f"{ARCH_CLOUD_BASE}/Arch-Linux-{a}-cloudimg.qcow2"] @@ -346,6 +374,8 @@ def default_image_name(distro: str, code: str, arch: str, version: str) -> str: return f"debian-{version}-genericcloud-{a}.qcow2" if distro == "arch": return f"arch-linux-{a}-cloudimg.qcow2" + if distro == "opensuse": + return f"opensuse-tumbleweed-minimal-vm-{a}.qcow2" if distro in ("almalinux", "rocky"): # « latest » est MUTABLE : le nom de cache porte donc la majeure, et # une image déjà téléchargée sera réutilisée telle quelle. C'est voulu @@ -1374,12 +1404,25 @@ def host_timezone() -> str: return "UTC" -def admin_group(distro: str) -> str: - """Groupe d'administration de la distribution. +def user_groups(distro: str) -> str: + """Groupes secondaires du compte créé par cloud-init. - Debian et Ubuntu : « sudo ». Famille RHEL (AlmaLinux, Rocky, Fedora) et - Arch : « wheel » — elles n'ont aucun groupe « sudo ».""" - return "sudo" if distro in ("ubuntu", "debian") else "wheel" + Le nom du groupe d'administration change d'une famille à l'autre, et un + nom INCONNU fait échouer « useradd -G » : l'utilisateur n'est alors pas + créé du tout, donc ni mot de passe ni clé SSH, et la VM démarre + inaccessible. Le privilège vient de toute façon de la directive « sudo: » + du cloud-config, pas du groupe — celui-ci n'est qu'une commodité. + + - Debian, Ubuntu : « sudo ». + - Famille RHEL (AlmaLinux, Rocky, CentOS, Fedora), Arch : « wheel ». + - openSUSE : aucun. Son cloud-init par défaut n'en met pas, et rien ne + garantit « wheel » sur une image Minimal-VM — dans le doute on s'abstient + plutôt que de risquer un compte non créé.""" + if distro in ("ubuntu", "debian"): + return "users, sudo" + if distro == "opensuse": + return "users" + return "users, wheel" def build_cloud_config( @@ -1400,7 +1443,7 @@ def build_cloud_config( # même écart, elles n'ont pas de groupe « sudo » mais « wheel ». # Le privilège lui-même vient de la ligne « sudo: » ci-dessus, pas du # groupe : celui-ci n'est qu'une commodité. - f" groups: users, {admin_group(args.distro)}", + f" groups: {user_groups(args.distro)}", " shell: /bin/bash", " lock_passwd: false" if pw_hash else " lock_passwd: true", ] diff --git a/script/todo/todo.py b/script/todo/todo.py index c3709f3..44c8d41 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1027,6 +1027,7 @@ class TODO: "fedora": (["41", "42", "43", "44"], "42"), "almalinux": (["9", "10"], "9"), "rocky": (["9", "10"], "10"), + "opensuse": (["tumbleweed"], "tumbleweed"), "arch": (["latest"], "latest"), } @@ -1074,13 +1075,20 @@ class TODO: # Distros publiant des images cloud par architecture (cohérent avec # S390X_DISTROS / ARM64_DISTROS de deploy_qemu.py). amd64 : toutes. - _QEMU_S390X_DISTROS = ("ubuntu", "almalinux", "rocky", "fedora") + _QEMU_S390X_DISTROS = ( + "ubuntu", + "almalinux", + "rocky", + "fedora", + "opensuse", + ) _QEMU_ARM64_DISTROS = ( "ubuntu", "debian", "fedora", "almalinux", "rocky", + "opensuse", ) # Alias distro pour l'affichage (jeton générique -> nom courant). _QEMU_ARCH_ALIAS = {"amd64": "x86_64", "arm64": "aarch64"} @@ -4325,6 +4333,7 @@ class TODO: "dnf_env": "graphical-server-environment " "workstation-product-environment gnome-desktop", "pacman": "gnome gdm", + "zypper": "patterns-gnome-gnome_basic gdm", "service": "gdm", }, "cinnamon": { @@ -4336,6 +4345,7 @@ class TODO: "apt": "cinnamon-desktop-environment dbus-x11", "dnf_env": "cinnamon-desktop", "pacman": "cinnamon lightdm lightdm-gtk-greeter", + "zypper": "cinnamon lightdm", "service": "lightdm", }, } @@ -4344,6 +4354,7 @@ class TODO: "apt": {"packages": "xrdp", "port": 3389, "client": "RDP"}, "dnf": {"packages": "xrdp", "port": 3389, "client": "RDP"}, "pacman": {"packages": "tigervnc", "port": 5901, "client": "VNC"}, + "zypper": {"packages": "xrdp", "port": 3389, "client": "RDP"}, } # Place que prend un bureau complet, annoncée dans le plan : sur une image # cloud de 40 G, l'oublier remplit le disque en pleine installation. @@ -4408,6 +4419,44 @@ class TODO: ">/dev/null 2>&1 || true; " ) + # Miroirs openSUSE préférés, du plus proche au dernier recours. Le + # redirecteur officiel n'est PAS géographique pour cette distribution : + # mesuré depuis Montréal sur les métadonnées oss s390x (15 Mo), + # download.opensuse.org met 23,8 s — il sert depuis l'Europe — contre + # 2,7 s pour mirrors.rit.edu. Les trois familles dnf, elles, choisissent + # déjà un miroir canadien toutes seules ; rien à faire de ce côté. + # + # Chaque miroir est SONDÉ sur le chemin de l'architecture ET du produit + # courants, puis le premier qui répond gagne. C'est nécessaire : aucun ne + # réplique tout. Relevé le 2026-08-12 — + # csclub Leap oui, Tumbleweed non (404) + # rit.edu zsystems oui ; injoignable ce jour-là (curl 7) + # leaseweb Tumbleweed x86_64 et Leap oui, ports zsystems non + # D'où plusieurs entrées plutôt qu'une : avec la seule rit.edu, sa panne + # renvoyait tout le monde sur download.opensuse.org, servi d'Europe. + # Ordonnées par proximité de Montréal. Aucun sondage concluant : on garde + # les dépôts de l'image, donc le comportement d'avant. + _QEMU_ZYPPER_MIRRORS = ( + "https://mirror.csclub.uwaterloo.ca/opensuse", + "https://mirrors.rit.edu/opensuse", + "https://mirror.us.leaseweb.net/opensuse", + ) + + def _qemu_zypper_mirror_cmd(self): + """Réécrit l'hôte des dépôts zypper vers un miroir plus proche.""" + mirrors = " ".join(self._QEMU_ZYPPER_MIRRORS) + return ( + "zp=tumbleweed; " + '[ "$(uname -m)" = s390x ] && zp=ports/zsystems/tumbleweed; ' + f"for zm in {mirrors}; do " + "if curl -fsS --max-time 20 -o /dev/null " + '"$zm/$zp/repo/oss/repodata/repomd.xml"; then ' + "sudo sed -i " + '"s|https\\?://download\\.opensuse\\.org|$zm|g" ' + "/etc/zypp/repos.d/*.repo 2>/dev/null || true; " + f'echo " {t("openSUSE mirror:")} $zm"; break; fi; done; ' + ) + def _qemu_desktop_remote_cmd(self, flavour="gnome"): """Bloc shell installant le bureau choisi + son accès distant, quelle que soit la distribution. Même aiguillage que l'installation ERPLibre, @@ -4440,6 +4489,14 @@ class TODO: "|| sudo rm -f /var/lib/pacman/db.lck; " f"sudo pacman -S --needed --noconfirm {de['pacman']} " f"{rem['pacman']['packages']}; " + "elif command -v zypper >/dev/null 2>&1; then " + "sudo zypper --non-interactive refresh || true; " + # « --auto-agree-with-licenses » appartient à la SOUS-COMMANDE + # install, pas aux options globales : placé avant, zypper répond + # « The flag --auto-agree-with-licenses is not known ». + "sudo zypper --non-interactive install " + f"--auto-agree-with-licenses {de['zypper']} " + f"{rem['zypper']['packages']}; " "else echo 'Gestionnaire de paquets inconnu'; exit 1; fi; " # Le bureau ne sert à rien s'il ne démarre pas tout seul : les # images cloud démarrent en multi-user.target. @@ -4550,10 +4607,29 @@ class TODO: "--save /etc/pacman.d/mirrorlist || true; " "sudo pacman -Syu --noconfirm || true; " "sudo pacman -S --needed --noconfirm $PKGS; " + "elif command -v zypper >/dev/null 2>&1; then " + # openSUSE : « --non-interactive » vaut le -y des autres, et + # « --auto-agree-with-licenses », qui va APRÈS « install », + # évite un blocage sur une licence à accepter. + # Tumbleweed étant rolling, on rafraîchit avant d'installer. + + self._qemu_zypper_mirror_cmd() + + "sudo zypper --non-interactive refresh || true; " + # Tumbleweed est ROLLING et ne supporte pas les mises à jour + # partielles, exactement comme Arch. L'image cloud est un + # instantané figé : ses dépôts ont avancé depuis, et un + # « install » simple bute sur une incohérence — vécu, git 2.54 + # réclamait perl-Git bâti contre un perl-base plus ancien que + # celui de l'image. zypper proposait alors trois solutions et + # attendait un choix ; « --non-interactive » prend le défaut, + # « c » = annuler, et l'installation s'arrêtait là. + "sudo zypper --non-interactive dup --auto-agree-with-licenses " + "--allow-vendor-change || true; " + "sudo zypper --non-interactive install " + "--auto-agree-with-licenses $PKGS; " "elif command -v yum >/dev/null 2>&1; then " "sudo yum makecache -q || true; sudo yum install -y $PKGS; " "else echo 'Aucun gestionnaire de paquets " - "(apt/dnf/pacman/yum)'; exit 1; fi; " + "(apt/dnf/pacman/zypper/yum)'; exit 1; fi; " # Vérifie explicitement que tout est là : erreur nette plutôt # qu'un « command not found » cryptique plus loin. "for t in curl git make; do command -v $t >/dev/null 2>&1 || " diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 4c5ffde..8ad421b 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2055,6 +2055,10 @@ TRANSLATIONS = { "fr": "Installation du bureau GNOME (long)", "en": "Installing the GNOME desktop (long)", }, + "openSUSE mirror:": { + "fr": "miroir openSUSE :", + "en": "openSUSE mirror:", + }, "Remote desktop:": { "fr": "Bureau distant :", "en": "Remote desktop:", From a579cc9cf3a818db038ab8ee10328c32ef5ecfd0 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 20:46:07 -0400 Subject: [PATCH 15/41] [FIX] install: the EL9, EL10 and Fedora build chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "dnf install: error: unrecognized arguments" on AlmaLinux 10 and Rocky 10, on every call: PostgreSQL, build dependencies, pyenv. The tools group got away with its fallback, which made the trace misleading -- gcc installed, nothing after it did. "--skip-unavailable" is a dnf5 option, which the script's own comment already said while assuming it everywhere. Fedora 41+ ships dnf5, but EL9 and EL10 stay on dnf4, whose equivalent is "--setopt=strict=0". We now ask dnf what it understands instead of inferring it from the distribution. Three neighbouring fixes: the "c-development" group does not exist on EL, where it is called "development"; CRB is enabled through /usr/bin/crb; and g++ plus the missing headers are installed explicitly. Rust is added only where wheels are absent, and qpdf is built when the distribution ships one older than pikepdf demands. --- FR --- « dnf install: error: unrecognized arguments » sur AlmaLinux 10 et Rocky 10, à chaque appel : PostgreSQL, dépendances de compilation, pyenv. Le groupe d'outils s'en tirait par son repli, ce qui rendait la trace trompeuse — gcc installé, rien après lui. « --skip-unavailable » est une option de dnf5, ce que le commentaire du script disait déjà tout en la supposant partout. Fedora 41+ livre dnf5, mais EL9 et EL10 restent sur dnf4, dont l'équivalent est « --setopt=strict=0 ». On demande maintenant à dnf ce qu'il comprend plutôt que de le déduire de la distribution. Trois correctifs voisins : le groupe « c-development » n'existe pas sur EL, où il s'appelle « development » ; CRB s'active par /usr/bin/crb ; et g++ ainsi que les en-têtes manquants sont posés explicitement. Rust n'est ajouté que là où les roues manquent, et qpdf est compilé quand la distribution en livre un plus ancien que ce qu'exige pikepdf. Assisted-by: Claude Opus 5 --- script/install/install_debian_dependency.sh | 33 +----- script/install/install_fedora_dependency.sh | 125 ++++++++++++++++++-- script/install/install_suse_dependency.sh | 1 + script/install/lib_qpdf.sh | 89 ++++++++++++++ 4 files changed, 209 insertions(+), 39 deletions(-) create mode 100644 script/install/lib_qpdf.sh diff --git a/script/install/install_debian_dependency.sh b/script/install/install_debian_dependency.sh index 8f1b15d..d8cb64f 100755 --- a/script/install/install_debian_dependency.sh +++ b/script/install/install_debian_dependency.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash . ./env_var.sh +. ./script/install/lib_qpdf.sh EL_USER=${USER} #EL_INSTALL_WKHTMLTOPDF="True" @@ -169,33 +170,11 @@ if [ "$(uname -m)" = "s390x" ]; then echo "Attention : libclang introuvable, la compilation de pymupdf va echouer." fi fi - # pikepdf n'a pas de roue s390x et se lie à qpdf, dont il exige une version - # PRÉCISE : 12.2.0 au minimum pour la 10.x. Ubuntu en livre 9.1.1 (20.04), - # 10.6.3 (22.04) et 11.9.0 (24.04) — trop anciennes, et aucune version de - # pikepdf ne descend sous qpdf 11.5. 25.10 et 26.04 passent, d'où le partage - # observé. On complète donc par les sources sous ce seuil ; /usr/local est - # déjà dans les chemins par défaut de g++ et de l'éditeur de liens. - QPDF_MIN=12.2.0 - QPDF_VER=12.3.2 - qpdf_have="$(pkg-config --modversion libqpdf 2>/dev/null || echo 0)" - if [ "$(printf '%s\n%s\n' "${QPDF_MIN}" "${qpdf_have}" | sort -V | head -1)" != "${QPDF_MIN}" ]; then - echo "qpdf ${qpdf_have} < ${QPDF_MIN} requis par pikepdf : compilation de qpdf ${QPDF_VER} (long en emulation)." - QPDF_TMP="$(mktemp -d)" - if curl -fsSL --max-time 600 -o "${QPDF_TMP}/qpdf.tar.gz" \ - "https://github.com/qpdf/qpdf/releases/download/v${QPDF_VER}/qpdf-${QPDF_VER}.tar.gz" && - tar -xzf "${QPDF_TMP}/qpdf.tar.gz" -C "${QPDF_TMP}" && - cmake -S "${QPDF_TMP}/qpdf-${QPDF_VER}" -B "${QPDF_TMP}/build" \ - -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \ - -DBUILD_DOC=OFF -DBUILD_STATIC_LIBS=OFF && - cmake --build "${QPDF_TMP}/build" -j"$(nproc)" && - sudo cmake --install "${QPDF_TMP}/build"; then - sudo ldconfig - echo "qpdf $(PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --modversion libqpdf 2>/dev/null) installe dans /usr/local." - else - echo "Attention : compilation de qpdf echouee, pikepdf ne pourra pas se construire." - fi - rm -rf "${QPDF_TMP}" - fi + # pikepdf se lie a qpdf, dont il exige 12.2.0 au minimum. Ubuntu 24.04 en + # livre 11.9.0 ; 25.10 et 26.04 passent, d'ou le partage observe. Le detail + # -- seuil, version batie, chemin d'installation -- est dans lib_qpdf.sh, + # partage avec les scripts dnf et zypper qui butaient sur le meme mur. + el_qpdf_ensure # cryptography ne publie aucune roue s390x : elle se compile, et son # Cargo.lock est en version 4, que seul cargo >= 1.78 sait lire. Ubuntu 24.04 # livre 1.75 et s'arrête sur « lock file version 4 requires diff --git a/script/install/install_fedora_dependency.sh b/script/install/install_fedora_dependency.sh index e530196..b60d38d 100755 --- a/script/install/install_fedora_dependency.sh +++ b/script/install/install_fedora_dependency.sh @@ -2,17 +2,33 @@ # © 2021-2026 TechnoLibre (http://www.technolibre.ca) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) # -# Dépendances système ERPLibre pour Fedora (dnf). Équivalent Fedora de -# install_debian_dependency.sh. « --skip-unavailable » tolère un nom de paquet -# absent (dnf5) au lieu d'échouer sur tout le lot. +# Dépendances système ERPLibre pour la famille dnf : Fedora, mais aussi +# AlmaLinux, Rocky et RHEL, qu'install_dev.sh aiguille ici sur ID_LIKE=rhel. +# Équivalent de install_debian_dependency.sh. +# +# Attention : Fedora 41+ livre dnf5, EL9 et EL10 encore dnf4. Les deux ne +# comprennent pas les mêmes options — voir DNF_SKIP plus bas. . ./env_var.sh +. ./script/install/lib_qpdf.sh EL_USER=${USER} +# « Sauter un paquet introuvable au lieu d'échouer sur tout le lot » ne +# s'écrit PAS pareil selon la version de dnf : « --skip-unavailable » est une +# option de dnf5 (Fedora 41+), et dnf4 — encore livré par EL9 et EL10, donc +# par AlmaLinux et Rocky — la refuse net : « dnf install: error: unrecognized +# arguments ». Son équivalent y est « --setopt=strict=0 ». On demande donc à +# dnf lui-même ce qu'il comprend, plutôt que de deviner d'après la distro. +if dnf install --help 2>&1 | grep -q -- "--skip-unavailable"; then + DNF_SKIP="--skip-unavailable" +else + DNF_SKIP="--setopt=strict=0" +fi +echo "dnf : option de tolerance retenue = ${DNF_SKIP}" # dnf résilient : rafraîchit le cache (évite « checksum doesn't match » / # signature après un cache périmé) et saute les paquets introuvables. -DNF="sudo dnf install -y --refresh --skip-unavailable" +DNF="sudo dnf install -y --refresh ${DNF_SKIP}" #-------------------------------------------------- # Dérivés RHEL : dépôts supplémentaires @@ -21,8 +37,8 @@ DNF="sudo dnf install -y --refresh --skip-unavailable" # les aiguille sur ID_LIKE=rhel). Contrairement à Fedora, leur jeu de dépôts # par défaut est ÉTROIT : la plupart des paquets « -devel » vivent dans CRB # (CodeReady Builder), et tout ce qui est communautaire dans EPEL. Sans ces -# deux dépôts, « --skip-unavailable » saute les paquets EN SILENCE et l'échec -# n'apparaît qu'à la compilation, très loin d'ici. +# deux dépôts, la tolérance aux paquets absents les saute EN SILENCE et +# l'échec n'apparaît qu'à la compilation, très loin d'ici. if [ -r /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release @@ -32,21 +48,106 @@ case "${ID}" in echo -e "\n---- Depots EPEL et CRB (famille RHEL) ----" sudo dnf install -y epel-release \ || echo "epel-release indisponible : certains paquets manqueront." - # « crb » sur EL9/EL10 ; « powertools » était le nom sur EL8. - sudo dnf config-manager --set-enabled crb 2> /dev/null \ + # /usr/bin/crb est l'outil fourni par epel-release, et ce que son propre + # scriptlet recommande d'exécuter — plus fiable que de deviner le nom du + # dépôt, qui vaut « crb » sur EL9/EL10 et « powertools » sur EL8. + sudo /usr/bin/crb enable 2> /dev/null \ + || sudo dnf config-manager --set-enabled crb 2> /dev/null \ || sudo dnf config-manager --set-enabled powertools 2> /dev/null \ || sudo dnf config-manager --enable crb 2> /dev/null \ || echo "CRB/PowerTools non active : certains -devel manqueront." + # Un dépôt fraîchement activé dont les métadonnées ne descendent pas fait + # échouer TOUS les dnf suivants. On le voit ici, avec son nom, plutôt que + # trois sections plus loin sur un paquet sans rapport. + sudo dnf makecache 2> /dev/null \ + || { sudo dnf clean all; sudo dnf makecache; } \ + || echo "Attention : metadonnees de depot incompletes." ;; esac #-------------------------------------------------- # Outils de compilation (build Python via pyenv, extensions Python) #-------------------------------------------------- -echo -e "\n---- Groupe outils de développement ----" -# Groupes par ID (le nom affiché « C Development Tools... » n'est pas matché). -sudo dnf group install -y --skip-unavailable development-tools c-development \ - || sudo dnf install -y gcc gcc-c++ make automake patch +echo -e "\n---- Outils de developpement ----" +# Les GROUPES ne sont pas les mêmes d'une famille à l'autre : Fedora a +# « c-development », EL ne connaît que « development » — vérifié dans le +# comps.xml d'AlmaLinux 10, qui n'a même pas « development-tools ». +# +# Pire, la tolérance aux paquets absents les rend INOFFENSIFS : un +# « dnf group install » de deux groupes inconnus REND ZÉRO sans rien poser, +# donc le « || » de repli ne se déclenchait jamais. gcc arrivait par la +# section pyenv plus bas, gcc-c++ par personne, et numpy s'arrêtait sur +# « Unknown compiler(s): [['c++'], ['g++'], …] ». +# +# On installe donc les compilateurs EXPLICITEMENT, sans dépendre d'un groupe. +# Le groupe reste ensuite, en complément et en best-effort. +${DNF} gcc gcc-c++ make automake patch +retVal=$? +if [[ $retVal -ne 0 ]]; then + echo "dnf install compilers error." + exit 1 +fi +sudo dnf group install -y ${DNF_SKIP} development c-development \ + > /dev/null 2>&1 || true + +#-------------------------------------------------- +# Mainframe s390x +#-------------------------------------------------- +# Sur s390x, AUCUNE roue PyPI n'existe : numpy, pillow, lxml, pikepdf, +# psycopg2, cryptography… tout se compile contre les bibliothèques de la +# distribution. Ces en-têtes ne servent à rien sur amd64, où pip pose des +# roues, mais leur absence ici arrête l'installation très loin de sa cause — +# « The headers or library files could not be found for jpeg » pour pillow. +if [ "$(uname -m)" = "s390x" ]; then + echo -e "\n---- Dependances de compilation s390x ----" + # Best-effort : un nom qui change d'une version à l'autre ne doit pas + # emporter le lot. Ce qui est vraiment indispensable est déjà installé + # au-dessus (compilateurs) ou plus bas (pyenv, PostgreSQL). + # « rust » et « cargo » : plusieurs paquets Python portent une extension + # RUST, pas seulement C — bcrypt et cryptography en tête. Sur amd64 leurs + # roues masquent le besoin ; ici tout compile, et bcrypt s'arrête net sur + # « error: can't find Rust compiler ». Les versions livrées suffisent + # (AlmaLinux 1.92, Fedora plus récent) au Cargo.lock v4 qui exige 1.78. + ${DNF} \ + rust cargo \ + libjpeg-turbo-devel zlib-devel geos-devel proj-devel \ + krb5-devel tbb-devel ninja-build clang-devel llvm-devel \ + GeographicLib-devel pkgconf-pkg-config cmake + + # qpdf : pikepdf en exige 12.2.0, et le paquet de la distribution ne le donne + # que sur Fedora. EL9 livre 10.3 — même pas « qpdf/QPDFJob.hh », d'où trois + # cents lignes de g++ sans un mot sur qpdf ; EL10 livre 11.9. + # + # On ne pose donc « qpdf-devel » QUE s'il atteint le seuil, au lieu de le + # poser puis de le contourner : contrairement à Debian, l'éditeur de liens de + # RHEL cherche /usr/lib64 AVANT /usr/local/lib64. Le -devel trop ancien ferait + # lier pikepdf contre l'ancienne bibliothèque malgré les en-têtes neufs de + # /usr/local/include — une incohérence bien plus difficile à lire qu'un + # en-tête absent. Sans lui, el_qpdf_ensure fournit tout depuis les sources. + qpdf_repo="$(dnf repoquery --qf '%{version}' --latest-limit 1 qpdf-devel \ + 2> /dev/null | tail -1)" + if el_qpdf_ge_min "${qpdf_repo}"; then + ${DNF} qpdf-devel + else + echo "qpdf-devel ${qpdf_repo:-absent} sous le seuil de pikepdf : compilation depuis les sources." + fi + el_qpdf_ensure + # Le dire ICI plutôt que de laisser bcrypt le découvrir une heure plus tard. + if ! command -v cargo > /dev/null 2>&1; then + echo "Attention : cargo absent, bcrypt et cryptography ne compileront pas." + fi + # pymupdf charge « libclang.so » par son nom nu, via ctypes. Le paquet le + # livre sous un nom versionné : il ne manque que le lien. + for d in /usr/lib64 /usr/lib; do + if [ -d "${d}" ] && [ ! -e "${d}/libclang.so" ]; then + so="$(ls -1 "${d}"/libclang.so.* 2> /dev/null | sort -V | tail -1)" + if [ -n "${so}" ]; then + sudo ln -s "${so}" "${d}/libclang.so" && sudo ldconfig + echo "libclang.so -> ${so}" + fi + fi + done +fi #-------------------------------------------------- # PostgreSQL diff --git a/script/install/install_suse_dependency.sh b/script/install/install_suse_dependency.sh index 9abddc2..a70a252 100755 --- a/script/install/install_suse_dependency.sh +++ b/script/install/install_suse_dependency.sh @@ -10,6 +10,7 @@ # demi-heure sous émulation s390x, ne s'y déclenche pas. . ./env_var.sh +. ./script/install/lib_qpdf.sh EL_USER=${USER} diff --git a/script/install/lib_qpdf.sh b/script/install/lib_qpdf.sh new file mode 100644 index 0000000..547aae3 --- /dev/null +++ b/script/install/lib_qpdf.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Bibliothèque SOURÇABLE : garantir un qpdf assez récent pour pikepdf. +# +# pikepdf 10 déclare QPDF_MIN_VERSION = "12.2.0" et se lie à la bibliothèque +# C++ de la distribution. Là où PyPI publie une roue, rien de tout cela ne +# s'exécute ; sur s390x il n'y en a aucune, pikepdf compile, et une version +# trop ancienne le fait échouer sur des en-têtes qui n'existent pas encore +# (« qpdf/QPDFJob.hh », apparu en 10.6 ; « qpdf/QPDFLogger.hh », en 11.0) puis +# sur des constantes renommées. Le message ne nomme jamais qpdf : on lit trois +# cents lignes de g++ avant de comprendre. +# +# Aucune famille n'est à jour partout, d'où le seuil plutôt qu'une liste de +# distributions : Ubuntu 24.04 livre 11.9, AlmaLinux et Rocky 9 livrent 10.3 +# (sans même QPDFJob.hh), leurs 10 livrent 11.9 ; Ubuntu 25.10, Fedora 43 et +# Tumbleweed dépassent le seuil et ne compilent rien. +# +# Ce fichier existe parce que le bloc vivait en double : corrigé côté apt, il +# ne l'était pas côté dnf, et EL9 est ressorti sur exactement le même mur le +# lendemain. Le seuil, la version et la façon de bâtir sont désormais écrits +# UNE fois pour les trois familles de paquets. + +EL_QPDF_MIN=12.2.0 +EL_QPDF_VER=12.3.2 + +# Vrai si la version passée en argument atteint le seuil. « sort -V » est le +# seul comparateur de versions disponible partout sans dépendance, mais il +# classe « 12.2 » AVANT « 12.2.0 » — un paquet numéroté sur deux composantes +# aurait donc déclenché une compilation d'une demi-heure pour rien. On complète +# les composantes manquantes avant de comparer. +el_qpdf_ge_min() { + local v="$1" dots + [ -n "${v}" ] || return 1 + dots="${v//[^.]/}" + while [ "${#dots}" -lt 2 ]; do + v="${v}.0" + dots="${dots}." + done + [ "$(printf '%s\n%s\n' "${EL_QPDF_MIN}" "${v}" | sort -V | head -1)" = "${EL_QPDF_MIN}" ] +} + +# Version actuellement visible du compilateur, ou « 0 ». +# /usr/local n'est pas dans le chemin par défaut de pkg-config sur toutes les +# familles : sans ces deux entrées, un qpdf déjà compilé passerait inaperçu et +# on le recompilerait à chaque passage du script. +el_qpdf_version() { + PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:/usr/local/lib64/pkgconfig:${PKG_CONFIG_PATH}" \ + pkg-config --modversion libqpdf 2> /dev/null || echo 0 +} + +# Compile et installe qpdf dans /usr/local si, et seulement si, ce qui est +# présent ne suffit pas. Ne rend jamais un code non nul : l'échec est signalé +# et l'installation continue, pikepdf dira lui-même ce qui manque. +el_qpdf_ensure() { + local have build dir + have="$(el_qpdf_version)" + if el_qpdf_ge_min "${have}"; then + echo "qpdf ${have} >= ${EL_QPDF_MIN} : rien a compiler pour pikepdf." + return 0 + fi + + echo "qpdf ${have} < ${EL_QPDF_MIN} requis par pikepdf : compilation de qpdf ${EL_QPDF_VER} (long en emulation)." + build="$(mktemp -d)" + if curl -fsSL --max-time 600 -o "${build}/qpdf.tar.gz" \ + "https://github.com/qpdf/qpdf/releases/download/v${EL_QPDF_VER}/qpdf-${EL_QPDF_VER}.tar.gz" \ + && tar -xzf "${build}/qpdf.tar.gz" -C "${build}" \ + && cmake -S "${build}/qpdf-${EL_QPDF_VER}" -B "${build}/build" \ + -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DBUILD_DOC=OFF -DBUILD_STATIC_LIBS=OFF \ + && cmake --build "${build}/build" -j"$(nproc)" \ + && sudo cmake --install "${build}/build"; then + # CMake choisit « lib » ou « lib64 » selon la famille, et /usr/local/lib64 + # n'est pas dans le chemin de ld.so d'EL : sans cette déclaration, pikepdf + # se construirait pour échouer au CHARGEMENT, encore plus loin de la cause. + for dir in /usr/local/lib64 /usr/local/lib; do + if [ -e "${dir}/libqpdf.so" ]; then + echo "${dir}" | sudo tee /etc/ld.so.conf.d/qpdf-local.conf > /dev/null + break + fi + done + sudo ldconfig + echo "qpdf $(el_qpdf_version) installe dans /usr/local." + else + echo "Attention : compilation de qpdf echouee, pikepdf ne pourra pas se construire." + fi + rm -rf "${build}" +} From 70d9ad456cde268436c8c598ca7dd8c097e8993b Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 22:03:40 -0400 Subject: [PATCH 16/41] [ADD] install: mise as Python provider, pyenv as fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mise lays down a precompiled CPython where pyenv builds one: seconds against one to three minutes, with no -dev package at all. The choice lives in EL_PYTHON_PROVIDER -- auto, mise or pyenv -- and in a single file, lib_python_provider.sh. The rest of the repository only knows venv paths and needs to know none of this. In auto mode an ALREADY installed interpreter wins, whichever provider put it there. mise is never installed on its own, "curl | sh" commits too much for a script to decide -- make install_mise carries that decision. MISE_PYTHON_COMPILE=false forbids it from quietly compiling: without that guard it would fall back to pyenv's own engine, giving us the slowness without the tooling. That is also what stopped gcc from collapsing while building CPython on a low-memory s390x guest, for nothing. One real bug falls along the way: a failed venv did not stop the install, which then went on and failed further down, far from the cause. --- FR --- mise pose un CPython précompilé là où pyenv en compile un : des secondes contre une à trois minutes, et aucun paquet -dev. Le choix vit dans EL_PYTHON_PROVIDER — auto, mise ou pyenv — et dans un seul fichier, lib_python_provider.sh. Le reste du dépôt ne connaît que des chemins de venv et n'a rien à savoir de tout cela. En mode auto, un interpréteur DÉJÀ posé l'emporte, quel qu'en soit le fournisseur. mise n'est jamais installé de lui-même, « curl | sh » engage trop pour qu'un script en décide — make install_mise porte cette décision. MISE_PYTHON_COMPILE=false lui interdit de compiler en silence : sans ce garde, il retomberait sur le moteur de pyenv, et nous aurions sa lenteur sans son outillage. C'est aussi ce qui a évité que gcc s'écroule en bâtissant CPython sur une VM s390x à faible mémoire, inutilement. Un vrai défaut tombe au passage : un venv raté n'arrêtait pas l'installation, qui continuait et échouait plus loin, loin de la cause. Assisted-by: Claude Opus 5 --- .claude/rules/08-deployment.md | 13 +- Makefile | 22 +++ README.base.md | 4 +- README.fr.md | 2 +- README.md | 2 +- env_var.sh | 5 + script/install/install_dev.sh | 5 + script/install/install_locally.sh | 15 +- script/install/install_suse_dependency.sh | 12 ++ script/install/install_venv.sh | 63 ++----- script/install/lib_python_provider.sh | 196 ++++++++++++++++++++++ script/todo/source_todo.sh | 19 ++- 12 files changed, 303 insertions(+), 55 deletions(-) create mode 100755 script/install/lib_python_provider.sh diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md index bd6c86a..354d4ce 100644 --- a/.claude/rules/08-deployment.md +++ b/.claude/rules/08-deployment.md @@ -8,8 +8,19 @@ Plateformes supportées : Ubuntu 24.04 / 25.10 / 26.04, Linux Mint 22.3, Debian 12, AlmaLinux 9+, Rocky Linux 9+, openSUSE Tumbleweed, Arch Linux, -macOS (pyenv), +macOS (mise ou pyenv), Windows (WSL/Docker). Ubuntu 20.04 et 22.04 sont abandonnées : pikepdf exige qpdf >= 12.2, compilé en C++20, quand focal livre GCC 9. + +## Interpréteur Python + +`EL_PYTHON_PROVIDER` (dans `env_var.sh`) vaut `auto`, `mise` ou `pyenv`. +`auto` reprend un interpréteur DÉJÀ posé, quel que soit le fournisseur ; sinon +il préfère mise, qui pose un CPython précompilé, et retombe sur pyenv, qui le +compile. + +Un seul fichier décide : `script/install/lib_python_provider.sh`. mise n'est +jamais installé automatiquement — `make install_mise` porte cette décision. +Pas de binaire mise pour s390x à ce jour : cette architecture reste sur pyenv. diff --git a/Makefile b/Makefile index 465190a..59b417e 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,28 @@ version: pyenv_update: ~/.pyenv/bin/pyenv update +# mise pose un CPython PRECOMPILE la ou pyenv le compile : quelques secondes +# contre une a trois minutes, et aucun paquet -dev requis. Volontairement +# EXPLICITE : « curl | sh » engage, et ce n'est pas a un script d'installation +# de le decider a la place de l'utilisateur. Une fois mise present, +# EL_PYTHON_PROVIDER=auto s'en sert tout seul. +# +# Pas de binaire s390x publie a ce jour : sur cette architecture, la cible +# le dit et n'installe rien. +.PHONY: install_mise +install_mise: + @if command -v mise >/dev/null 2>&1; then \ + echo "mise deja present : $$(mise --version)"; \ + elif command -v pacman >/dev/null 2>&1; then \ + sudo pacman -S --needed --noconfirm mise; \ + elif [ "$$(uname -m)" = "s390x" ]; then \ + echo "mise ne publie pas de binaire s390x : on reste sur pyenv."; \ + else \ + echo "Installation de mise depuis https://mise.run"; \ + curl -fsSL https://mise.run | sh; \ + echo "Ajoutez ~/.local/bin a votre PATH, puis relancez l installation."; \ + fi + .PHONY: db_create_db_test db_create_db_test: ./script/make.sh db_drop_db_test diff --git a/README.base.md b/README.base.md index af2bd49..d79149d 100644 --- a/README.base.md +++ b/README.base.md @@ -95,7 +95,7 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. # Supported platforms - **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux -- **macOS** : via pyenv +- **macOS** : via mise ou pyenv - **Windows** : via WSL or Docker - **Mainframe** : 390x architecture @@ -104,7 +104,7 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. # Plateformes supportées - **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux -- **macOS** : via pyenv +- **macOS** : via mise ou pyenv - **Windows** : via WSL ou Docker - **Mainframe** : architecture 390x diff --git a/README.fr.md b/README.fr.md index 1af228c..5c80a26 100644 --- a/README.fr.md +++ b/README.fr.md @@ -48,7 +48,7 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. # Plateformes supportées - **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux -- **macOS** : via pyenv +- **macOS** : via mise ou pyenv - **Windows** : via WSL ou Docker - **Mainframe** : architecture 390x diff --git a/README.md b/README.md index bf6847e..90bbb1b 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc. # Supported platforms - **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux -- **macOS** : via pyenv +- **macOS** : via mise ou pyenv - **Windows** : via WSL or Docker - **Mainframe** : 390x architecture diff --git a/env_var.sh b/env_var.sh index c899ad9..d338d3e 100755 --- a/env_var.sh +++ b/env_var.sh @@ -14,6 +14,11 @@ EL_POETRY_VERSION=$(cat ".poetry-version" | xargs) EL_PYTHON_ODOO_VERSION=$(cat ".python-odoo-version" | xargs) EL_PYTHON_ERPLIBRE_VERSION=$(cat "./conf/python-erplibre-version" | xargs) EL_ERPLIBRE_VERSION=$(cat ".erplibre-version" | xargs) +# Fournisseur d'interpreteur Python : auto | mise | pyenv. +# « auto » prend ce qui est DEJA pose, sinon mise s'il est present (Python +# precompile, quelques secondes), sinon pyenv (compilation). Voir +# script/install/lib_python_provider.sh. +EL_PYTHON_PROVIDER="${EL_PYTHON_PROVIDER:-auto}" # The default port where this Odoo instance will run under (provided you use the command -c in the terminal) # Set to true if you want to install it, false if you don't need it or have it already installed. EL_INSTALL_WKHTMLTOPDF="True" diff --git a/script/install/install_dev.sh b/script/install/install_dev.sh index 3e6f8f7..5f3228d 100755 --- a/script/install/install_dev.sh +++ b/script/install/install_dev.sh @@ -1,4 +1,9 @@ #!/usr/bin/env bash +# Le code de retour du script de distribution DOIT remonter : il est aujourd'hui +# la derniere commande executee, donc propage par accident. « set -e » le rend +# explicite, et update_env_version.py:818 s'en sert pour arreter l'installation +# au lieu d'enchainer sur un systeme sans compilateur. +set -e # « $OSTYPE » ne decrit PAS le systeme : bash le fige au triplet avec lequel il # a ete compile. openSUSE batit bash en « x86_64-suse-linux » et le renseigne diff --git a/script/install/install_locally.sh b/script/install/install_locally.sh index 585fe26..38609e4 100755 --- a/script/install/install_locally.sh +++ b/script/install/install_locally.sh @@ -50,10 +50,21 @@ if [[ "${EL_PHASE}" != "poetry" ]]; then if [[ ! -n "${DOCKER_BUILD}" ]]; then # Install ERPLibre venv echo -e "Install ${VENV_ERPLIBRE_PATH} with ${EL_PYTHON_ERPLIBRE_VERSION}" - ./script/install/install_venv.sh "ERPLibre" "${VENV_ERPLIBRE_PATH}" "${EL_PYTHON_ERPLIBRE_VERSION}" + # Le code de retour n'était PAS regardé : quand l'interpréteur ne + # pouvait pas être obtenu, le script continuait, et tout ce qui suit + # échouait à son tour sur un venv inexistant. Le log devenait une + # cascade de « No such file or directory » répartie sur trois fichiers, + # où la cause réelle — pas de compilateur C — se perdait tout en haut. + if ! ./script/install/install_venv.sh "ERPLibre" "${VENV_ERPLIBRE_PATH}" "${EL_PYTHON_ERPLIBRE_VERSION}"; then + echo "Echec de creation de ${VENV_ERPLIBRE_PATH}, arret." + exit 1 + fi # Install Odoo venv echo -e "Install ${VENV_ODOO_PATH} with ${EL_PYTHON_ODOO_VERSION}" - ./script/install/install_venv.sh "Odoo" "${VENV_ODOO_PATH}" "${EL_PYTHON_ODOO_VERSION}" + if ! ./script/install/install_venv.sh "Odoo" "${VENV_ODOO_PATH}" "${EL_PYTHON_ODOO_VERSION}"; then + echo "Echec de creation de ${VENV_ODOO_PATH}, arret." + exit 1 + fi else mkdir .venv fi diff --git a/script/install/install_suse_dependency.sh b/script/install/install_suse_dependency.sh index a70a252..e9c86cc 100755 --- a/script/install/install_suse_dependency.sh +++ b/script/install/install_suse_dependency.sh @@ -273,6 +273,18 @@ if [[ ${retVal} -ne 0 ]]; then fi # Dépendances de build pour pyenv (compilation de CPython) — CRITIQUE. +# Python de la DISTRIBUTION d'abord : Tumbleweed livre python312 en 3.12.13, +# qui satisfait « >=3.12.10,<3.13 ». lib_python_provider.sh le prefere alors a +# toute compilation — decisif sur s390x, ou gcc 15.2 s'arrete sur une erreur +# interne en batissant Parser/parser.c de CPython 3.12.10. +# Le nom suit la version demandee : python312 pour 3.12.x. +PY_WANT="$(cat .python-odoo-version 2>/dev/null | xargs)" +PY_PKG="python$(echo "${PY_WANT}" | awk -F. '{print $1 $2}')" +if [ -n "${PY_WANT}" ]; then + echo -e "\n---- Python ${PY_WANT} depuis la distribution (${PY_PKG}) ----" + zyp_soft "${PY_PKG}" "${PY_PKG}-devel" "${PY_PKG}-pip" +fi + echo -e "\n---- Dependances pyenv (compilation Python) ----" zyp_in \ make gcc zlib-devel libbz2-devel readline-devel sqlite3-devel \ diff --git a/script/install/install_venv.sh b/script/install/install_venv.sh index 3019feb..4f4dc8f 100755 --- a/script/install/install_venv.sh +++ b/script/install/install_venv.sh @@ -17,52 +17,25 @@ echo "Context: $CONTEXT" echo "Venv Path: $VENV_PATH" echo "Python Version: $PYTHON_VERSION" -PYENV_PATH=~/.pyenv -PYENV_VERSION_PATH=${PYENV_PATH}/versions/${PYTHON_VERSION} -PYTHON_EXEC=${PYENV_VERSION_PATH}/bin/python -echo "Python path version home" -echo ${PYENV_VERSION_PATH} -#echo "Python path version local" -#echo ${LOCAL_PYTHON_EXEC} +# Le CHOIX du fournisseur (mise ou pyenv) vit dans la bibliothèque : ce script +# ne connaît qu'un chemin d'interpréteur. C'est ce qui permet d'en ajouter un +# troisième sans toucher ici. +# shellcheck source=script/install/lib_python_provider.sh +. ./script/install/lib_python_provider.sh -if [[ ! -d "${PYENV_PATH}" ]]; then - echo -e "\n---- Installing pyenv in ${PYENV_PATH} ----" - # export PYENV_GIT_TAG=v2.3.35 - # To change version - # rm ~/.pyenv to uninstall it - curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash +PYTHON_EXEC="$(el_python_exec "${PYTHON_VERSION}")" +if [[ -z "${PYTHON_EXEC}" ]] || [[ ! -x "${PYTHON_EXEC}" ]]; then + echo "Aucun interpreteur Python ${PYTHON_VERSION} n'a pu etre obtenu." + echo " Fournisseur demande : ${EL_PYTHON_PROVIDER:-auto}" + echo " Voir 'make install_mise', ou installez pyenv." + exit 1 fi - -echo -e "\n---- Export pyenv in ${PYENV_PATH} ----" -export PATH="${PYENV_PATH}/bin:$PATH" -eval "$(pyenv init -)" -eval "$(pyenv virtualenv-init -)" - -if [[ ! -d "${PYENV_VERSION_PATH}" ]]; then - echo -e "\n---- Installing python ${PYTHON_VERSION} with pyenv in ${PYENV_VERSION_PATH} ----" - # Update all python version list - cd "${PYENV_PATH}" && git pull && cd - - yes n|pyenv install ${PYTHON_VERSION} - if [[ $retVal -ne 0 ]]; then - echo -e "${Red}Error${Color_Off} when installing pyenv" - exit 1 - fi -fi - -# This will write .python-version -# pyenv local ${PYTHON_VERSION} +echo "Interpreteur retenu : ${PYTHON_EXEC}" if [[ ! -d ${VENV_PATH} ]]; then - echo -e "\n---- Create Virtual environment Python ----" - if [[ -e ${PYTHON_EXEC} ]]; then - ${PYTHON_EXEC} -m venv "${VENV_PATH}" - retVal=$? - if [[ $retVal -ne 0 ]]; then - echo "Virtual environment, error when creating ${VENV_PATH}" - exit 1 - fi - else - echo "Missing pyenv, please refer installation guide. Check variable '${PYTHON_EXEC}'." - exit 1 - fi -fi \ No newline at end of file + echo -e "\n---- Create Virtual environment Python ----" + if ! "${PYTHON_EXEC}" -m venv "${VENV_PATH}"; then + echo "Virtual environment, error when creating ${VENV_PATH}" + exit 1 + fi +fi diff --git a/script/install/lib_python_provider.sh b/script/install/lib_python_provider.sh new file mode 100755 index 0000000..211d62e --- /dev/null +++ b/script/install/lib_python_provider.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Bibliothèque SOURÇABLE : « une version de Python » -> « un interpréteur ». +# +# C'est le seul endroit du dépôt qui décide COMMENT un interpréteur est +# obtenu. Tout le reste ne consomme que des chemins de venv, jamais pyenv ni +# mise. Ajouter un fournisseur se fait donc ici, et nulle part ailleurs. +# +# Deux fournisseurs : +# pyenv historique. Compile CPython depuis les sources : 1 à 3 min sur une +# machine récente, bien plus sous émulation, et il lui faut une +# douzaine de -dev (openssl, zlib, readline, sqlite, bzip2, xz, tk…). +# mise pose un CPython PRÉCOMPILÉ (astral python-build-standalone) : +# quelques secondes, aucun compilateur requis. +# +# EL_PYTHON_PROVIDER choisit : auto (défaut), mise, ou pyenv. +# +# En mode « auto », un interpréteur DÉJÀ présent gagne, quel que soit le +# fournisseur qui l'a posé. Une installation pyenv qui marche n'est donc +# jamais doublée par un second CPython de 150 Mo. + +# Ne jamais laisser mise compiler en silence : sans build précompilée pour la +# plateforme, il retomberait sur python-build — le moteur de pyenv — et on +# aurait la lenteur de pyenv sans son outillage. « false » = télécharger ou +# échouer ; l'échec nous fait basculer proprement sur le repli. +export MISE_PYTHON_COMPILE=false + +el_pyenv_root() { + echo "${PYENV_ROOT:-${HOME}/.pyenv}" +} + +# Chemin de l'interpréteur pyenv d'une version, qu'il existe ou non. +el_pyenv_exec_path() { + echo "$(el_pyenv_root)/versions/$1/bin/python" +} + +# Interpréteur déjà posé par mise, sans le moindre accès réseau. +el_mise_exec_path() { + command -v mise > /dev/null 2>&1 || return 1 + local out + out="$(mise where "python@$1" 2> /dev/null)" || return 1 + [ -n "${out}" ] && [ -x "${out}/bin/python" ] || return 1 + echo "${out}/bin/python" +} + +# Vrai si l'exécutable rend EXACTEMENT la version demandée. Un interpréteur +# présent mais d'une autre version casserait Poetry, dont le pyproject exige +# le patch au près (>=3.12.10,<3.13). +el_python_is_version() { + local exe="$1" want="$2" got + [ -x "${exe}" ] || return 1 + got="$("${exe}" -c 'import platform;print(platform.python_version())' 2> /dev/null)" + [ "${got}" = "${want}" ] +} + +# Vrai si l'exécutable CONVIENT : même majeure.mineure, et patch au moins +# égal au demandé. C'est exactement ce qu'exige le pyproject — « >=3.12.10, +# <3.13 » — et non l'égalité stricte que testait el_python_is_version. +# +# La distinction n'est pas théorique. Tumbleweed s390x livre python312 en +# 3.12.13 : parfaitement utilisable, mais rejeté par l'égalité, ce qui forçait +# pyenv à COMPILER CPython — et gcc 15.2 s'y arrête sur une erreur interne +# dans Parser/parser.c, un fichier généré de quarante mille lignes. +el_python_is_compatible() { + local exe="$1" want="$2" got + [ -x "${exe}" ] || return 1 + got="$("${exe}" -c 'import platform;print(platform.python_version())' \ + 2> /dev/null)" + [ -n "${got}" ] || return 1 + # Même majeure.mineure : 3.13 ne convient pas à un pyproject borné à <3.13. + [ "${got%.*}" = "${want%.*}" ] || return 1 + # Patch au moins égal, comparé en version et non en chaîne (3.12.9 < 3.12.10). + [ "$(printf '%s\n%s\n' "${want}" "${got}" | sort -V | head -1)" = "${want}" ] +} + +# Interpréteur de la DISTRIBUTION qui conviendrait, s'il y en a un. +# +# Cherché avant toute compilation : sur une architecture émulée, bâtir CPython +# prend des dizaines de minutes quand il aboutit. Le nom suit la convention de +# toutes les distributions, « python3.12 ». +el_distro_python_exec() { + local want="$1" exe + # Des chemins SYSTEME, jamais « command -v » : dans un venv activé celui-ci + # rend le python DU VENV, et l'on bâtirait un venv depuis un venv. Le PATH + # d'une session interactive n'a rien à faire dans cette décision. + for exe in "/usr/bin/python${want%.*}" "/usr/local/bin/python${want%.*}"; do + if el_python_is_compatible "${exe}" "${want}"; then + echo "${exe}" + return 0 + fi + done + return 1 +} + +# Installe la version via mise. Renvoie le chemin, ou échoue. +el_mise_install() { + local version="$1" exe + command -v mise > /dev/null 2>&1 || return 1 + echo "---- Python ${version} via mise (precompile) ----" >&2 + mise install "python@${version}" >&2 || return 1 + exe="$(el_mise_exec_path "${version}")" || return 1 + el_python_is_version "${exe}" "${version}" || return 1 + echo "${exe}" +} + +# Installe la version via pyenv, en posant pyenv lui-même au besoin. +el_pyenv_install() { + local version="$1" root exe + root="$(el_pyenv_root)" + exe="$(el_pyenv_exec_path "${version}")" + if [[ ! -d "${root}" ]]; then + echo "---- Installation de pyenv dans ${root} ----" >&2 + curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer \ + | bash >&2 || return 1 + fi + export PATH="${root}/bin:$PATH" + eval "$(pyenv init - 2> /dev/null)" || true + eval "$(pyenv virtualenv-init - 2> /dev/null)" || true + if [[ ! -d "${root}/versions/${version}" ]]; then + # pyenv COMPILE CPython : sans compilateur C, il télécharge l'archive, + # lance configure et s'arrête sur « no acceptable C compiler found », après + # avoir gaspillé le téléchargement. On le dit AVANT, et on nomme le paquet. + if ! command -v cc > /dev/null 2>&1 \ + && ! command -v gcc > /dev/null 2>&1; then + echo "Aucun compilateur C : pyenv ne peut pas compiler Python." >&2 + echo " Installez le necessaire de compilation (build-essential," >&2 + echo " gcc/gcc-c++, ou le motif devel_basis) puis relancez." >&2 + return 1 + fi + echo "---- Python ${version} via pyenv (compilation) ----" >&2 + # La liste des versions connues vient du dépôt git de pyenv : sans ce + # « pull », une version récente est « not a known version ». + (cd "${root}" && git pull) >&2 || true + # Le contrôle d'erreur d'origine testait un $retVal jamais affecté ici : + # un échec de compilation passait inaperçu jusqu'au test d'existence. + if ! yes n | pyenv install "${version}" >&2; then + echo "pyenv install ${version} a echoue." >&2 + return 1 + fi + fi + el_python_is_version "${exe}" "${version}" || return 1 + echo "${exe}" +} + +# API publique : imprime le chemin absolu d'un interpréteur de cette version, +# ou rien (et rend non nul) si aucun fournisseur n'y parvient. +el_python_exec() { + local version="$1" provider="${EL_PYTHON_PROVIDER:-auto}" exe + + # 1) Déjà présent ? On ne réinstalle rien et on ne touche pas au réseau. + # C'est ce qui rend le changement sans effet pour une installation + # pyenv existante. + if [ "${provider}" != "mise" ]; then + exe="$(el_pyenv_exec_path "${version}")" + if el_python_is_version "${exe}" "${version}"; then + echo "${exe}" + return 0 + fi + fi + if [ "${provider}" != "pyenv" ]; then + if exe="$(el_mise_exec_path "${version}")" \ + && el_python_is_version "${exe}" "${version}"; then + echo "${exe}" + return 0 + fi + fi + + # 2) Un interpréteur de la DISTRIBUTION qui convient ? Rien à installer, + # rien à compiler. Vérifié avant mise et pyenv : c'est le seul chemin + # qui ne coûte rien, et sur s390x le seul qui aboutisse partout — mise + # n'y publie aucun binaire, et pyenv doit compiler. + # Uniquement en mode « auto » : demander mise ou pyenv explicitement doit + # être respecté, sinon le réglage ne veut plus rien dire. + if [ "${provider}" = "auto" ] \ + && exe="$(el_distro_python_exec "${version}")"; then + echo "Python $("${exe}" -V 2>&1 | awk '{print $2}') de la distribution :" \ + "aucune compilation." >&2 + echo "${exe}" + return 0 + fi + + # 3) Rien de posé : il faut provisionner. mise d'abord quand il est là, + # parce qu'il télécharge au lieu de compiler. + if [ "${provider}" != "pyenv" ]; then + if exe="$(el_mise_install "${version}")"; then + echo "${exe}" + return 0 + fi + [ "${provider}" = "mise" ] && return 1 + command -v mise > /dev/null 2>&1 \ + && echo "mise n'a pas pu fournir Python ${version} : repli sur pyenv." >&2 + fi + el_pyenv_install "${version}" +} diff --git a/script/todo/source_todo.sh b/script/todo/source_todo.sh index 84082d2..4a31863 100755 --- a/script/todo/source_todo.sh +++ b/script/todo/source_todo.sh @@ -7,10 +7,23 @@ DIR_VENV_ERPLIBRE=".venv.erplibre" if [[ ! -d "$DIR_VENV_ERPLIBRE" ]]; then DIR_VENV_ERPLIBRE_EXIST=0 echo "$DIR_VENV_ERPLIBRE does not exist." - if [[ ! -d "~/.pyenv/versions/$(< ./conf/python-erplibre-version)/bin/python" ]]; then - "~/.pyenv/versions/$(< ./conf/python-erplibre-version)/bin/python" -m venv $DIR_VENV_ERPLIBRE + # Le test d'origine etait inoperant : le tilde entre guillemets n'est pas + # etendu, et « -d » testait un FICHIER comme un repertoire. La condition + # etait donc toujours vraie, la commande echouait en « No such file or + # directory », et le repli « python -m venv » ne servait jamais. + # shellcheck source=script/install/lib_python_provider.sh + . ./script/install/lib_python_provider.sh + EL_PY_WANT="$(< ./conf/python-erplibre-version)" + # Ici on ne PROVISIONNE pas : ce script s'execute au demarrage de TODO, il + # doit rester rapide et hors reseau. On prend ce qui est deja pose, sinon + # le python du systeme. + EL_PY_EXEC="$(el_pyenv_exec_path "${EL_PY_WANT}")" + el_python_is_version "${EL_PY_EXEC}" "${EL_PY_WANT}" \ + || EL_PY_EXEC="$(el_mise_exec_path "${EL_PY_WANT}" 2> /dev/null)" + if [[ -n "${EL_PY_EXEC}" ]] && [[ -x "${EL_PY_EXEC}" ]]; then + "${EL_PY_EXEC}" -m venv $DIR_VENV_ERPLIBRE else - python -m venv $DIR_VENV_ERPLIBRE + python3 -m venv $DIR_VENV_ERPLIBRE fi fi From 7f6c18037ddc8e5d47d416edb104a21f859cafd6 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 22:11:23 -0400 Subject: [PATCH 17/41] [ADD] tui qemu: choose mise or pyenv at deploy time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interpreter provider is now chosen when creating a VM, in both interfaces. "mise" installs it in the VM and lays down a precompiled CPython; "pyenv" keeps today's build-from-source. The question is only asked where it means something: mise publishes binaries for amd64 and arm64 only. An all-s390x fleet never sees it, a mixed fleet sees it along with the architectures that will fall back to pyenv -- said before deploying, not discovered in a log an hour later. mise goes into /usr/local/bin rather than ~/.local/bin: the remote command runs through "ssh host 'command'", where neither ~/.profile nor ~/.bashrc is read. Choosing "mise" exports "auto", not "mise", so a failed install can still fall back to pyenv. The unavailability notice was translated as "pyenv required", which said the opposite of what happens: pyenv is the fallback, not a demand. --- FR --- Le fournisseur d'interpréteur se choisit désormais à la création d'une VM, dans les deux interfaces. « mise » l'installe dans la VM et pose un CPython précompilé ; « pyenv » garde la compilation d'aujourd'hui. La question n'est posée que là où elle a un sens : mise ne publie de binaires que pour amd64 et arm64. Un parc tout s390x ne la voit jamais, un parc mixte la voit avec les architectures qui retomberont sur pyenv — dit avant de déployer, pas découvert dans un log une heure plus tard. mise va dans /usr/local/bin plutôt que ~/.local/bin : la commande distante passe par « ssh hôte 'commande' », où ni ~/.profile ni ~/.bashrc ne sont lus. Choisir « mise » exporte « auto », pas « mise », pour qu'une installation ratée puisse encore retomber sur pyenv. L'avis d'indisponibilité était traduit par « pyenv exigé », qui disait l'inverse de ce qui se passe : pyenv est le repli, pas une exigence. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 54 +++++++++++++++++++ script/todo/todo.py | 93 +++++++++++++++++++++++++++++++-- script/todo/todo_i18n.py | 28 ++++++++++ 3 files changed, 171 insertions(+), 4 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 83658f3..edc53bc 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -178,6 +178,7 @@ def build_spec(vms, domains, form): "ssh_key": form["ssh_key"], "timezone": form.get("timezone", ""), "desktop": form.get("desktop", ""), + "python_provider": form.get("python_provider", ""), "install": form["install"], "add_ssh_config": form["add_ssh_config"], "parallelism": form["parallelism"], @@ -245,6 +246,8 @@ def run_deploy_form(ctx, run_app: bool = True): desktop_disk = ctx.get("desktop_disk_gb") or 0 # [(clé, libellé)] — la liste vient de todo.py, source unique. desktops = list(ctx.get("desktops") or []) + # Architectures pour lesquelles mise publie un binaire. + mise_arches = set(ctx.get("mise_arches") or ()) defaults = ctx.get("defaults") or {} result = {"spec": None} @@ -508,6 +511,18 @@ def run_deploy_form(ctx, run_app: bool = True): value=defaults.get("add_ssh_config", True), id="f_sshcfg", ) + # mise pose un CPython précompilé, pyenv le compile. + # Grisé quand AUCUNE des VM retenues n'est sur une + # architecture que mise sert. + yield Static( + t("Python interpreter:"), classes="grouptitle" + ) + with RadioSet(id="f_python"): + yield RadioButton( + t("mise (precompiled, faster)"), value=True + ) + yield RadioButton(t("pyenv (compiles from source)")) + yield Static("", id="miswarn") yield Static(t("Parallelism"), classes="grouptitle") # Cochée, la case donne une exécution PAR installation : # le plafond du nombre de CPU ne s'applique plus. Décochée, @@ -600,6 +615,44 @@ def run_deploy_form(ctx, run_app: bool = True): grow += desktop_disk self.rows = plan_rows(self.vms, domains, grow) self._render_plan() + self._render_mise() + + def _render_mise(self): + """Grise le choix quand aucune VM retenue n'est servie par mise, + et nomme les architectures qui retomberont sur pyenv.""" + usable = self._mise_usable() + self.query_one("#f_python", RadioSet).disabled = not usable + skipped = sorted( + { + vm["arch"] + for vm in self.vms + if vm["arch"] not in mise_arches + } + ) + msg = "" + if skipped: + msg = ( + f" ⚠ {t('mise has no binary for:')} " + f"{', '.join(skipped)} — {t('those VMs use pyenv')}" + ) + self.query_one("#miswarn", Static).update(msg) + + def _python_provider(self): + """« mise », « pyenv », ou rien — c'est-à-dire « automatique ». + + Rien, et surtout pas « pyenv », quand mise n'est servi par aucune + architecture retenue. « mise est indisponible » ne veut pas dire + « l'utilisateur exige pyenv » : la nuance décide de tout, puisqu'un + choix EXPLICITE écarte le Python de la distribution. Sur s390x, + renvoyer « pyenv » forçait la compilation de CPython — celle dont + gcc 15.2 ne revient pas.""" + if not self._mise_usable(): + return "" + index = self.query_one("#f_python", RadioSet).pressed_index + return "pyenv" if index == 1 else "mise" + + def _mise_usable(self): + return any(vm["arch"] in mise_arches for vm in self.vms) def _desktop(self): """« » pour un serveur, sinon la clé de la saveur choisie.""" @@ -783,6 +836,7 @@ def run_deploy_form(ctx, run_app: bool = True): or ctx.get("timezone") or "", "desktop": self._desktop(), + "python_provider": self._python_provider(), "install": install, "add_ssh_config": self.query_one("#f_sshcfg", Checkbox).value, # Une exécution par installation : le nombre de VM retenues diff --git a/script/todo/todo.py b/script/todo/todo.py index 44c8d41..e1ecaeb 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4512,8 +4512,51 @@ class TODO: '(vncpasswd puis vncserver :1)"; fi; ' ) + # mise ne publie de binaire que pour ces architectures : 46 assets à la + # v2026.8.4, aucun s390x — son propre script d'installation refuse cette + # plateforme. Ailleurs, le choix « mise » est sans objet et on reste sur + # pyenv, ce que le formulaire et l'invite disent avant de déployer. + QEMU_MISE_ARCHES = ("amd64", "arm64") + + def _qemu_mise_remote_cmd(self, python_provider): + """Pose mise DANS la VM et fixe EL_PYTHON_PROVIDER pour l'installation. + + mise s'installe par défaut dans ~/.local/bin, qui n'est PAS dans le + PATH d'un « ssh hôte 'commande' » : ni ~/.profile ni ~/.bashrc n'y sont + lus. On le pose donc dans /usr/local/bin, présent dans le PATH par + défaut — même raison que pour cargo et rustc. + + Sans mise utilisable, rien n'est écrit : lib_python_provider.sh + retombe alors sur pyenv toute seule.""" + if python_provider == "pyenv": + # Explicite : même si mise se trouvait déjà dans l'image, on ne + # l'utilise pas. Sans cela le mode « auto » du dépôt le prendrait. + return "export EL_PYTHON_PROVIDER=pyenv; " + if python_provider != "mise": + return "" + return ( + f'echo "== {t("Installing mise (precompiled Python)")} =="; ' + "if command -v mise >/dev/null 2>&1; then " + 'echo " mise: $(mise --version)"; ' + "else " + # La variable est passée À sudo, pas exportée avant : « sudo -E » + # dépend de env_reset dans sudoers et n'est pas garanti. + "curl -fsSL https://mise.run " + "| sudo MISE_INSTALL_PATH=/usr/local/bin/mise sh " + '|| echo " mise indisponible ici : pyenv prendra le relais"; ' + "fi; " + # « auto », et non « mise » : si l'installation ci-dessus a échoué, + # lib_python_provider.sh doit pouvoir retomber sur pyenv. + "export EL_PYTHON_PROVIDER=auto; " + ) + def _qemu_erplibre_remote_cmd( - self, branch, final_cmd=None, prod=False, desktop=False + self, + branch, + final_cmd=None, + prod=False, + desktop=False, + python_provider="", ): """Script exécuté DANS la VM. `branch` à None n'installe QUE le bureau — le choix graphique ne dépend pas d'ERPLibre, et une VM peut être @@ -4521,7 +4564,9 @@ class TODO: `final_cmd` par défaut : install_os + install_odoo_18. `prod` : installe dans /opt/erplibre (au lieu de ~/git/erplibre) + service - SELinux confiné. `desktop` : ajoute GNOME et son accès distant.""" + SELinux confiné. `desktop` : ajoute GNOME et son accès distant. + `python_provider` : « mise » pour un CPython précompilé, sinon le + comportement par défaut du dépôt (pyenv, qui compile).""" if not branch: # Bureau seul : ni clone ni make, mais on garde le prologue — # attente de cloud-init et coupure des mises à jour automatiques, @@ -4635,6 +4680,7 @@ class TODO: "for t in curl git make; do command -v $t >/dev/null 2>&1 || " '{ echo "Outil manquant apres installation: $t ' '(reseau de la VM ?)"; exit 1; }; done; ' + + self._qemu_mise_remote_cmd(python_provider) # Clone : /opt/erplibre en PROD (racine, puis chown à l'utilisateur # pour que make/venv s'exécutent sans sudo), ~/git/erplibre en dev. + ( @@ -4665,6 +4711,7 @@ class TODO: final_cmd=None, prod=False, desktop=False, + python_provider="", ): """Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre le dashboard Textual. Quitter le dashboard n'arrête pas les installs. @@ -4677,7 +4724,7 @@ class TODO: ) remote = self._qemu_erplibre_remote_cmd( - branch, final_cmd, prod, desktop + branch, final_cmd, prod, desktop, python_provider ) try: mod = self._qemu_import_module() @@ -4742,6 +4789,7 @@ class TODO: final_cmd=None, prod=False, desktop=False, + python_provider="", ): """Clone ERPLibre (branche donnée) dans la VM puis exécute la commande d'install du profil choisi (streamé). `ip` : IP déjà résolue ; @@ -4763,7 +4811,7 @@ class TODO: ) return remote = self._qemu_erplibre_remote_cmd( - branch, final_cmd, prod, desktop + branch, final_cmd, prod, desktop, python_provider ) ssh_opts = ( "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " @@ -5145,6 +5193,9 @@ class TODO: print( f" {t('VM type:')} {t('Graphical (server + desktop):')} {label}" ) + prov = spec.get("python_provider") + if prov: + print(f" {t('Python interpreter:')} {prov}") print(f" {t('SSH key:')} {spec.get('ssh_key') or t('none')}") cfg = ( t("one entry per VM") if spec["add_ssh_config"] else t("untouched") @@ -5472,6 +5523,7 @@ class TODO: "disk_presets": self._QEMU_DISK_PRESETS, "extra_disk_gb": self.ERPLIBRE_EXTRA_DISK_GB, "desktop_disk_gb": self.QEMU_DESKTOP_EXTRA_DISK_GB, + "mise_arches": self.QEMU_MISE_ARCHES, "desktops": [ (k, v["label"]) for k, v in self._QEMU_DESKTOP.items() ], @@ -5762,6 +5814,32 @@ class TODO: return "" return flavours[index] if 0 <= index < len(flavours) else "" + def _qemu_ask_python_provider(self, arches): + """mise (CPython précompilé) ou pyenv (compilation). + + `arches` : les architectures du parc à déployer. mise ne publie pas de + binaire pour toutes — hors de QEMU_MISE_ARCHES la question n'a pas de + sens et on ne la pose pas.""" + usable = [a for a in arches if a in self.QEMU_MISE_ARCHES] + if not usable: + # Rien, pas « pyenv » : le mode automatique doit rester libre de + # préférer un Python de la distribution. Voir _python_provider() + # dans le formulaire, même raisonnement. + return "" + print(f"\n{t('Python interpreter:')}") + print(f" [1] {t('mise (precompiled, faster)')} *") + print(f" [2] {t('pyenv (compiles from source)')}") + skipped = [a for a in arches if a not in self.QEMU_MISE_ARCHES] + if skipped: + # Dit AVANT le déploiement plutôt que découvert dans un log. + print( + f" ⚠ {t('mise has no binary for:')} " + f"{', '.join(sorted(set(skipped)))} — " + f"{t('those VMs use pyenv')}" + ) + sel = input(t("Choice (number, blank = mise): ")).strip() + return "pyenv" if sel == "2" else "mise" + def _qemu_host_timezone(self): """Fuseau de l'hôte. Défini une seule fois, dans deploy_qemu.py, qui est aussi ce qui l'écrit dans le cloud-config : l'invite ne peut donc @@ -5821,6 +5899,9 @@ class TODO: timezone = self._qemu_ask_timezone() locale = self._qemu_ask_locale() desktop = self._qemu_ask_desktop() + python_provider = self._qemu_ask_python_provider( + [vm["arch"] for vm in vms] + ) # 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM. install = None @@ -5898,6 +5979,7 @@ class TODO: "timezone": timezone, "locale": locale, "desktop": desktop, + "python_provider": python_provider, "install": install, "add_ssh_config": add_ssh_config, "parallelism": parallelism, @@ -5966,6 +6048,7 @@ class TODO: install = spec.get("install") install_branch = install["branch"] if install else None desktop = spec.get("desktop") or "" + python_provider = spec.get("python_provider") or "" ssh_key = spec.get("ssh_key") add_ssh_config = spec["add_ssh_config"] parallelism = spec["parallelism"] @@ -6040,6 +6123,7 @@ class TODO: install["cmd"] if install else None, install["prod"] if install else False, desktop=desktop, + python_provider=python_provider, ) else: print( @@ -6055,6 +6139,7 @@ class TODO: install["cmd"], install["prod"], desktop=desktop, + python_provider=python_provider, ) # Sommaire TOTAL (déploiement + résolution IP + ssh_config + install diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 8ad421b..43019ce 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2023,6 +2023,34 @@ TRANSLATIONS = { "fr": "Choix (numéro, vide = rouvrir) : ", "en": "Choice (number, blank = reopen): ", }, + "Python interpreter:": { + "fr": "Interpreteur Python :", + "en": "Python interpreter:", + }, + "mise (precompiled, faster)": { + "fr": "mise (precompile, plus rapide)", + "en": "mise (precompiled, faster)", + }, + "pyenv (compiles from source)": { + "fr": "pyenv (compile depuis les sources)", + "en": "pyenv (compiles from source)", + }, + "Choice (number, blank = mise): ": { + "fr": "Choix (numero, vide = mise) : ", + "en": "Choice (number, blank = mise): ", + }, + "mise has no binary for:": { + "fr": "mise ne publie pas de binaire pour :", + "en": "mise has no binary for:", + }, + "those VMs use pyenv": { + "fr": "ces VM utiliseront pyenv", + "en": "those VMs use pyenv", + }, + "Installing mise (precompiled Python)": { + "fr": "Installation de mise (Python precompile)", + "en": "Installing mise (precompiled Python)", + }, "VM type:": { "fr": "Type de VM :", "en": "VM type:", From e4aea8b8a6e12faecbbe6fbf980c4d99a04aa9b9 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 22:29:33 -0400 Subject: [PATCH 18/41] [ADD] arch: Canadian pacman mirrors first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured from Montréal on extra.db: mirror.quantum5.ca 2.0 s, mirror.xenyth.net 7.1 s, against 8.0 s for geo.mirror.pkgbuild.com. The official "geographic" mirror is therefore not the best one here, and reflector, which sorts by throughput from inside the VM, had not picked them. Both are PREPENDED, never substituted: whatever reflector chose stays below, as fallback. The write therefore comes after it, since "--save" overwrites the file. Guarded on x86_64, these mirrors serving that architecture only. Host side, the addition is idempotent. The desktop install went through neither step: it ran without the near mirrors and without the full update a rolling release demands, so it pulled from Europe and broke on partial upgrades. --- FR --- Mesuré depuis Montréal sur extra.db : mirror.quantum5.ca 2,0 s, mirror.xenyth.net 7,1 s, contre 8,0 s pour geo.mirror.pkgbuild.com. Le miroir « géographique » officiel n'est donc pas le meilleur ici, et reflector, qui trie par débit depuis la VM, ne les avait pas retenus. Les deux sont mis EN TÊTE, jamais substitués : ce que reflector a choisi reste dessous, en repli. L'écriture vient donc après lui, puisque « --save » écrase le fichier. Gardé sur x86_64, ces miroirs ne servant que cette architecture. Côté hôte, l'ajout est idempotent. L'installation du bureau ne passait par aucune des deux étapes : elle s'exécutait sans les miroirs proches et sans la mise à jour complète qu'exige une rolling release, tirant donc d'Europe et cassant sur des mises à jour partielles. Assisted-by: Claude Opus 5 --- script/install/install_arch_linux.sh | 15 +++++ script/todo/todo.py | 83 +++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/script/install/install_arch_linux.sh b/script/install/install_arch_linux.sh index 3289b17..d16c11b 100755 --- a/script/install/install_arch_linux.sh +++ b/script/install/install_arch_linux.sh @@ -16,6 +16,21 @@ EL_USER=${USER} # pacman résilient : --needed saute ce qui est déjà là, --noconfirm en non # interactif. PAC="sudo pacman -S --needed --noconfirm" +# Miroirs canadiens EN TÊTE de la mirrorlist. Mesuré depuis Montreal sur +# extra.db : quantum5 2,0 s, xenyth 7,1 s, contre 8,0 s pour +# geo.mirror.pkgbuild.com — le miroir « geographique » officiel n'est donc pas +# le meilleur ici. Les deux ne servent que x86_64 (Arch Linux ARM a ses propres +# miroirs), d'où la garde. On les AJOUTE en tete sans effacer l'existant : les +# miroirs deja configures restent en repli. +if [ "$(uname -m)" = "x86_64" ] \ + && ! grep -q "mirror.quantum5.ca" /etc/pacman.d/mirrorlist 2> /dev/null; then + echo -e "\n---- Miroirs pacman canadiens ----" + printf 'Server = https://mirror.quantum5.ca/archlinux/$repo/os/$arch\nServer = https://mirror.xenyth.net/archlinux/$repo/os/$arch\n' \ + | sudo tee /etc/pacman.d/mirrorlist.el > /dev/null + sudo sh -c 'cat /etc/pacman.d/mirrorlist >> /etc/pacman.d/mirrorlist.el \ + && mv /etc/pacman.d/mirrorlist.el /etc/pacman.d/mirrorlist' +fi + # Mise à jour COMPLÈTE obligatoire : Arch est en rolling release et NE SUPPORTE # PAS les mises à jour partielles. Sur une image cloud dont la glibc est # ancienne, un « pacman -S » lie le binaire à une glibc plus diff --git a/script/todo/todo.py b/script/todo/todo.py index e1ecaeb..0c8039a 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4442,6 +4442,71 @@ class TODO: "https://mirror.us.leaseweb.net/opensuse", ) + # Miroirs Arch canadiens, du plus rapide au suivant. Mesuré depuis + # Montréal sur extra.db : quantum5 2,0 s, xenyth 7,1 s, contre 8,0 s pour + # geo.mirror.pkgbuild.com — le miroir « géographique » officiel n'est donc + # pas le meilleur ici. Arch n'est proposé qu'en amd64 dans le catalogue, + # et ces deux-là ne servent que x86_64 (Arch Linux ARM a ses propres + # miroirs) : la garde d'architecture le dit quand même. + _QEMU_PACMAN_MIRRORS = ( + "https://mirror.quantum5.ca/archlinux/$repo/os/$arch", + "https://mirror.xenyth.net/archlinux/$repo/os/$arch", + ) + + def _qemu_pacman_mirror_cmd(self): + """Place les miroirs canadiens EN TÊTE de la mirrorlist. + + reflector écrase le fichier avec « --save » : il faut donc écrire + après lui, pas avant. Ses miroirs restent dessous, comme repli.""" + # « \\n » et non un vrai saut de ligne : la commande distante est UNE + # chaîne, passée à bash -c après shlex.quote. Un retour littéral y + # survivrait, mais rendrait la chaîne illisible et fragile à relire. + # « $repo » et « $arch » restent littéraux : c'est pacman qui les + # substitue, d'où les guillemets SIMPLES autour du format. + lines = "".join(f"Server = {m}\\n" for m in self._QEMU_PACMAN_MIRRORS) + first = self._QEMU_PACMAN_MIRRORS[0].split("/")[2] + return ( + '[ "$(uname -m)" = x86_64 ] && { ' + # Idempotent : la préparation Arch passe deux fois quand une VM + # est graphique (bureau puis ERPLibre), et empiler les mêmes + # miroirs à chaque passage allongerait la liste sans rien gagner. + f'grep -q "{first}" /etc/pacman.d/mirrorlist 2>/dev/null || {{ ' + f"printf '{lines}' | sudo tee /etc/pacman.d/mirrorlist.el " + "> /dev/null; " + "sudo sh -c 'cat /etc/pacman.d/mirrorlist " + ">> /etc/pacman.d/mirrorlist.el " + "&& mv /etc/pacman.d/mirrorlist.el /etc/pacman.d/mirrorlist'; " + "}; }; " + ) + + def _qemu_pacman_prepare_cmd(self): + """Préparation Arch : verrou, miroirs proches, mise à jour COMPLÈTE. + + Les trois sont indissociables, et il faut les faire AVANT la moindre + installation. Une image cloud Arch est un instantané dont la base de + paquets pointe des versions déjà retirées des miroirs : « pacman -S » + s'y arrête sur « failed retrieving file … 404 » — vécu sur llvm-libs + et perl. Arch ne supporte pas la mise à jour partielle. + + Ce bloc ne vivait QUE dans le chemin ERPLibre. Or le bureau s'installe + AVANT lui : une VM graphique échouait donc toujours, sans jamais + atteindre le code qui l'aurait sauvée.""" + return ( + "if command -v pacman >/dev/null 2>&1; then " + # Verrou périmé (cloud-init interrompu) : le retirer SEULEMENT si + # aucun pacman ne tourne, sinon on attend qu'il se libère. + "pgrep -x pacman >/dev/null 2>&1 " + "|| sudo rm -f /var/lib/pacman/db.lck; " + # reflector d'abord, nos miroirs ensuite : « --save » écrase le + # fichier, écrire avant lui ne servirait à rien. + "sudo pacman -Sy --needed --noconfirm reflector || true; " + "sudo reflector --latest 20 --protocol https --sort rate " + "--save /etc/pacman.d/mirrorlist || true; " + + self._qemu_pacman_mirror_cmd() + + "sudo pacman -Syu --noconfirm || true; " + "fi; " + ) + def _qemu_zypper_mirror_cmd(self): """Réécrit l'hôte des dépôts zypper vers un miroir plus proche.""" mirrors = " ".join(self._QEMU_ZYPPER_MIRRORS) @@ -4487,7 +4552,8 @@ class TODO: "elif command -v pacman >/dev/null 2>&1; then " "pgrep -x pacman >/dev/null 2>&1 " "|| sudo rm -f /var/lib/pacman/db.lck; " - f"sudo pacman -S --needed --noconfirm {de['pacman']} " + + self._qemu_pacman_prepare_cmd() + + f"sudo pacman -S --needed --noconfirm {de['pacman']} " f"{rem['pacman']['packages']}; " "elif command -v zypper >/dev/null 2>&1; then " "sudo zypper --non-interactive refresh || true; " @@ -4639,19 +4705,8 @@ class TODO: "sudo dnf install -y --refresh $PKGS || " "{ sudo dnf clean all; sudo dnf install -y --refresh $PKGS; }; " "elif command -v pacman >/dev/null 2>&1; then " - # Verrou pacman périmé (cloud-init interrompu) : le retirer SEULEMENT - # si aucun pacman ne tourne, sinon on attend qu'il se libère. - "pgrep -x pacman >/dev/null 2>&1 " - "|| sudo rm -f /var/lib/pacman/db.lck; " - # Arch : reflector sélectionne les miroirs HTTPS les plus rapides, - # puis MISE À JOUR COMPLÈTE (-Syu) : Arch (rolling) ne supporte pas - # les màj partielles ; sur une image cloud à la glibc ancienne, un - # « pacman -S » casse avec « GLIBC_x.yy not found ». - "sudo pacman -Sy --needed --noconfirm reflector || true; " - "sudo reflector --latest 20 --protocol https --sort rate " - "--save /etc/pacman.d/mirrorlist || true; " - "sudo pacman -Syu --noconfirm || true; " - "sudo pacman -S --needed --noconfirm $PKGS; " + + self._qemu_pacman_prepare_cmd() + + "sudo pacman -S --needed --noconfirm $PKGS; " "elif command -v zypper >/dev/null 2>&1; then " # openSUSE : « --non-interactive » vaut le -y des autres, et # « --auto-agree-with-licenses », qui va APRÈS « install », From e20d8d9baa10ff39d13dc2710e511a0c592947c8 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 23:16:26 -0400 Subject: [PATCH 19/41] [ADD] install: uv to place Python packages, pip as fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uv replaces pip where it can: the tools venv and the Poetry bootstrap. The choice lives in EL_PIP_PROVIDER -- auto, uv or pip -- and in a single file, lib_pip_provider.sh, on the exact model of the mise/pyenv switch. A uv failure falls back to pip: it is stricter on metadata and rejects packages pip accepts. Three guards. The target always goes through "--python" rather than being inferred from the active venv: poetry.toml declares a "./.venv" uv would also look for. Python 3.7, still used by Odoo 12 and 13, stays on pip. And "uv pip sync" is never used: it would delete pip itself and everything Poetry laid down. One bug falls along the way: the idempotence guard aimed at .venv.erplibre/bin/poetry while Poetry installs into the Odoo venv, so a successful replay reported failure all the same. --- FR --- uv remplace pip là où il le peut : le venv d'outils et l'amorçage de Poetry. Le choix vit dans EL_PIP_PROVIDER — auto, uv ou pip — et dans un seul fichier, lib_pip_provider.sh, sur le modèle exact de l'aiguillage mise/pyenv. Un échec d'uv retombe sur pip : il est plus strict sur les métadonnées et refuse des paquets que pip accepte. Trois gardes. La cible passe toujours par « --python » plutôt que d'être déduite du venv actif : poetry.toml déclare un « ./.venv » qu'uv chercherait aussi. Python 3.7, encore utilisé par Odoo 12 et 13, reste sur pip. Et « uv pip sync » n'est jamais employé : il supprimerait pip lui-même et tout ce que Poetry a posé. Un défaut tombe au passage : le garde d'idempotence visait .venv.erplibre/bin/poetry alors que Poetry s'installe dans le venv Odoo, si bien qu'un rejeu réussi rapportait quand même un échec. Assisted-by: Claude Opus 5 --- .claude/rules/08-deployment.md | 13 +++++ Makefile | 19 +++++++ env_var.sh | 6 +++ script/install/install_locally.sh | 81 +++++++++++++++++++++--------- script/install/lib_pip_provider.sh | 70 ++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 23 deletions(-) create mode 100755 script/install/lib_pip_provider.sh diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md index 354d4ce..a928e1a 100644 --- a/.claude/rules/08-deployment.md +++ b/.claude/rules/08-deployment.md @@ -24,3 +24,16 @@ compile. Un seul fichier décide : `script/install/lib_python_provider.sh`. mise n'est jamais installé automatiquement — `make install_mise` porte cette décision. Pas de binaire mise pour s390x à ce jour : cette architecture reste sur pyenv. + +## Paquets Python + +`EL_PIP_PROVIDER` (dans `env_var.sh`) vaut `auto`, `uv` ou `pip`. `auto` prend +uv s'il est présent et si le venv visé est en Python ≥ 3.8, sinon pip ; un +échec d'uv retombe sur pip. Un seul fichier décide : +`script/install/lib_pip_provider.sh`. uv n'est jamais installé +automatiquement — `make install_uv` porte cette décision. + +Portée réelle : le venv d'outils et l'amorçage de Poetry. **`poetry install` +n'est pas concerné** — uv ne lit pas `poetry.lock` — et c'est pourtant l'étape +qui domine. Sur s390x le gain est quasi nul : une trentaine de paquets sans +roue se compilent, et uv n'enlève pas une seconde de gcc. diff --git a/Makefile b/Makefile index 59b417e..6b7e893 100644 --- a/Makefile +++ b/Makefile @@ -105,6 +105,25 @@ pyenv_update: # # Pas de binaire s390x publie a ce jour : sur cette architecture, la cible # le dit et n'installe rien. +# uv installe les paquets Python nettement plus vite que pip, et met en cache +# les roues qu'il CONSTRUIT — ce qui compte la ou rien n'a de roue publiee. +# Contrairement a mise, uv publie une roue s390x : un « pip install uv » dans +# le venv d'outils suffit, sans « curl | sh ». Reste EXPLICITE quand meme, +# comme install_mise : EL_PIP_PROVIDER=auto s'en sert des qu'il est la. +# +# Attention a l'attente : « poetry install », qui domine le temps +# d'installation, n'est PAS accelere — uv ne lit pas poetry.lock. +.PHONY: install_uv +install_uv: + @if command -v uv >/dev/null 2>&1; then \ + echo "uv deja present : $$(uv --version)"; \ + else \ + echo "Installation de uv dans le venv d outils"; \ + ./$$(cat conf/python-erplibre-venv | xargs)/bin/pip install uv; \ + echo "uv : ./$$(cat conf/python-erplibre-venv | xargs)/bin/uv"; \ + echo "Ajoutez ce repertoire au PATH, ou installez uv globalement."; \ + fi + .PHONY: install_mise install_mise: @if command -v mise >/dev/null 2>&1; then \ diff --git a/env_var.sh b/env_var.sh index d338d3e..179fafc 100755 --- a/env_var.sh +++ b/env_var.sh @@ -19,6 +19,12 @@ EL_ERPLIBRE_VERSION=$(cat ".erplibre-version" | xargs) # precompile, quelques secondes), sinon pyenv (compilation). Voir # script/install/lib_python_provider.sh. EL_PYTHON_PROVIDER="${EL_PYTHON_PROVIDER:-auto}" +# Installateur de paquets Python : auto | uv | pip. +# « auto » prend uv s'il est present et si le venv vise est en Python >= 3.8, +# sinon pip ; un echec d'uv retombe sur pip. Voir +# script/install/lib_pip_provider.sh. Note : « poetry install » n'est PAS +# concerne, uv ne lit pas poetry.lock. +EL_PIP_PROVIDER="${EL_PIP_PROVIDER:-auto}" # The default port where this Odoo instance will run under (provided you use the command -c in the terminal) # Set to true if you want to install it, false if you don't need it or have it already installed. EL_INSTALL_WKHTMLTOPDF="True" diff --git a/script/install/install_locally.sh b/script/install/install_locally.sh index 38609e4..fe71f79 100755 --- a/script/install/install_locally.sh +++ b/script/install/install_locally.sh @@ -19,15 +19,27 @@ Color_Off='\033[0m' # Text Reset PYTHON_VERSION_MAJOR=$(echo "$EL_PYTHON_ODOO_VERSION" | sed 's/\.[^\.]*$//') VENV_ERPLIBRE_PATH=$(cat "conf/python-erplibre-venv" | xargs) VENV_ODOO_PATH=".venv.${EL_ERPLIBRE_VERSION}" -POETRY_ODOO_PATH=${VENV_ERPLIBRE_PATH}/bin/poetry +# Poetry est installé dans le venv ODOO, pas dans celui des outils : le garde +# d'idempotence visait le mauvais chemin, donc il était TOUJOURS vrai. Sans +# conséquence visible — poetry se réinstallait pour rien — mais le jour où ce +# fichier aurait existé, « poetry install » aurait été purement sauté. +POETRY_ODOO_PATH=${VENV_ODOO_PATH}/bin/poetry + +# Choix pip/uv : un seul endroit décide, comme pour mise/pyenv. +# shellcheck source=script/install/lib_pip_provider.sh +. ./script/install/lib_pip_provider.sh export WITH_POETRY_INSTALLATION=1 -# Verbosité de l'installation Poetry : silencieuse (-q) par défaut, les logs -# détaillés (-vvv) reviennent avec la variable d'environnement EL_VERBOSE=1. +# Verbosité de l'installation Poetry. « -q » a été retiré du défaut : dans Cleo, +# le mode silencieux coupe AUSSI la sortie d'erreur. Mesuré — « poetry -q +# commande-inexistante » rend 1 sans imprimer un caractère, quand la même sans +# « -q » nomme le problème. Un échec devenait donc parfaitement muet, et c'est +# pour cela qu'il avait fallu ajouter un rejeu de diagnostic plus bas. +# La verbosité par défaut de Poetry tient en une ligne par paquet. if [[ "${EL_VERBOSE:-0}" == "1" ]]; then POETRY_VERBOSE="-vvv" else - POETRY_VERBOSE="-q" + POETRY_VERBOSE="" fi # EL_PHASE controls which steps to execute. Used by install_locally_dev.sh @@ -72,7 +84,8 @@ if [[ "${EL_PHASE}" != "poetry" ]]; then source ./${VENV_ERPLIBRE_PATH}/bin/activate echo -e "Upgrade pip to ${VENV_ERPLIBRE_PATH}" pip install --upgrade pip - pip install -r requirement/erplibre_require-ments.txt + el_pip_install "${VENV_ERPLIBRE_PATH}" \ + -r requirement/erplibre_require-ments.txt ./script/install/install_git_repo.sh fi @@ -121,28 +134,50 @@ if [[ "${EL_PHASE}" != "setup" ]]; then esac fi - # Delete artifacts created by pip, cause error in next "poetry install" - if [[ ! -f "${POETRY_ODOO_PATH}" ]]; then + # Le garde ne couvre QUE l'amorçage de Poetry. « poetry install » doit + # rejouer à chaque fois : c'est lui qui applique un lock régénéré par + # « make poetry_update ». L'englober rendrait la mise à jour sans effet + # dès la seconde installation. + if [[ ! -x "${POETRY_ODOO_PATH}" ]]; then echo -e "Install Poetry ${POETRY_ODOO_PATH}" - pip install ${PIP_CONSTRAINT_CRYPTO} poetry==${EL_POETRY_VERSION} - poetry --version - # Fix broken poetry by installing ignored dependence - # poetry lock --no-update - # To fix keyring problem when installation is blocked, use - export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring - if [[ ${WITH_POETRY_INSTALLATION} -ne 0 ]]; then - poetry install --no-root ${POETRY_VERBOSE} - fi + el_pip_install "${VENV_ODOO_PATH}" \ + ${PIP_CONSTRAINT_CRYPTO} "poetry==${EL_POETRY_VERSION}" + fi + # Chemin EXPLICITE, jamais le « poetry » du PATH. Le script active pourtant + # le bon venv juste avant, mais poetry.toml porte « virtualenvs.create = + # false » : hors activation, Poetry installe dans l'interpréteur de BASE. + # Vérifié à la dure sur une VM — 772 paquets posés dans le CPython de mise, + # invisibles du venv qui a « include-system-site-packages = false », et + # promis à polluer tout autre venv bâti sur le même interpréteur. + if [[ ! -x "${POETRY_ODOO_PATH}" ]]; then + echo "Poetry introuvable a ${POETRY_ODOO_PATH}, arret." + exit 1 + fi + "${POETRY_ODOO_PATH}" --version + # To fix keyring problem when installation is blocked, use + export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring + # « poetry install » reste à Poetry : uv ne lit pas poetry.lock + # (astral-sh/uv#1804, « not planned ») et Poetry 2.1.3 n'a plus « export ». + if [[ ${WITH_POETRY_INSTALLATION} -ne 0 ]]; then + "${POETRY_ODOO_PATH}" install --no-root ${POETRY_VERBOSE} retVal=$? if [[ $retVal -ne 0 ]]; then echo "Poetry installation error with status ${retVal}" - # « -q » masque la CAUSE. On rejoue en « -vvv » (debug) car c'est - # le SEUL niveau où Poetry affiche la sortie des sous-processus - # (git clone/checkout, build pip) — donc l'erreur réelle d'une - # dépendance VCS/build. Capturé dans le log pour diagnostic. - echo "---- Poetry: rejeu -vvv pour diagnostic ----" - poetry install --no-root -vvv 2>&1 || true - exit 1 + # Un rejeu, parce qu'une partie des échecs sont des aléas de + # téléchargement : le lock est figé, rien ne dépend de l'instant. + # « -vvv » est le SEUL niveau où Poetry montre la sortie des + # sous-processus (git clone, build pip), donc l'erreur réelle. + # + # Le « exit 1 » était INCONDITIONNEL : un rejeu réussi laissait une + # installation complète... et la déclarait quand même en échec. + # C'est ce qui a arrêté openSUSE Leap 16 sur un pdfminer-six posé + # correctement au second essai. + echo "---- Poetry: rejeu -vvv (diagnostic et seconde chance) ----" + if "${POETRY_ODOO_PATH}" install --no-root -vvv 2>&1; then + echo "Le rejeu a REUSSI : le premier echec etait transitoire." + else + exit 1 + fi fi fi diff --git a/script/install/lib_pip_provider.sh b/script/install/lib_pip_provider.sh new file mode 100755 index 0000000..c4c3b39 --- /dev/null +++ b/script/install/lib_pip_provider.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Bibliothèque SOURÇABLE : installer des paquets Python dans un venv donné. +# +# Seul endroit du dépôt qui décide COMMENT les paquets sont posés. Deux +# fournisseurs, même contrat de sortie : +# pip historique, toujours présent dans un venv. +# uv nettement plus rapide sur la résolution, le téléchargement et la +# pose ; il met aussi en cache les roues qu'il CONSTRUIT, ce qui +# compte là où rien n'a de roue publiée. +# +# EL_PIP_PROVIDER choisit : auto (défaut), uv, ou pip. +# +# CE QUE ÇA NE FAIT PAS — et il faut le savoir avant d'en attendre trop : +# « poetry install --no-root » n'est PAS concerné. uv ne lit pas poetry.lock +# (astral-sh/uv#1804, fermé « not planned ») et Poetry 2.1.3 n'a plus la +# commande « export ». Or c'est cette étape qui domine : 354 paquets contre +# 153 pour le venv d'outils. Le gain porte donc sur le venv d'outils et +# l'amorçage de Poetry, pas sur le gros morceau. +# +# Et sur s390x il est quasi nul : une trentaine de paquets binaires n'ont +# aucune roue (numpy, pandas, pillow, lxml, pymupdf…) et se COMPILENT. uv +# n'enlève pas une seconde de gcc ; il parallélise seulement entre paquets. + +# uv ne supporte pas officiellement Python 3.7, encore utilisé par Odoo 12 +# et 13. En dessous de ce seuil on reste sur pip, sans discuter. +EL_UV_MIN_PY_MINOR=8 + +el_uv_usable() { + # $1 = interpréteur du venv visé. + local py="$1" minor + command -v uv > /dev/null 2>&1 || return 1 + [ -x "${py}" ] || return 1 + minor="$("${py}" -c 'import sys;print(sys.version_info[1])' 2> /dev/null)" + [ -n "${minor}" ] || return 1 + [ "${minor}" -ge "${EL_UV_MIN_PY_MINOR}" ] 2> /dev/null || return 1 +} + +# API publique : installe dans le venv `$1`, avec les arguments qui suivent. +# +# La CIBLE est toujours explicite, jamais déduite de l'environnement. uv +# cherche sinon un « .venv » dans le répertoire courant ou un parent, et +# poetry.toml en déclare justement un — le dépôt porte déjà la trace de cette +# confusion, des dépendances du pyproject Odoo s'étant retrouvées dans +# .venv.erplibre. « --python » ferme la question. +# +# « uv pip sync » n'est jamais utilisé : il SUPPRIME ce qui n'est pas dans le +# fichier d'entrée, donc pip lui-même et tout ce que Poetry a posé. +el_pip_install() { + local venv="$1" + shift + local py="${venv}/bin/python" + local provider="${EL_PIP_PROVIDER:-auto}" + + if [ "${provider}" != "pip" ] && el_uv_usable "${py}"; then + echo " installation par uv ($(uv --version 2>&1 | head -1))" + if uv pip install --python "${py}" "$@"; then + return 0 + fi + if [ "${provider}" = "uv" ]; then + return 1 + fi + # uv est plus strict que pip sur les métadonnées : il refuse des paquets + # que pip accepte. Le repli n'est donc pas théorique. + echo " uv a echoue : reprise avec pip." >&2 + fi + "${py}" -m pip install "$@" +} From 9c470f065e565dfb89cce847145b8f05c3d6a468 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 01:16:17 -0400 Subject: [PATCH 20/41] [ADD] qemu: openSUSE Leap 16.0, numbered, as the default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalogue only offered openSUSE Tumbleweed, a rolling release with no version number. Nearly every openSUSE incident in this series came from that: the cloud image is a snapshot lagging behind its own repos, hence the mandatory "zypper dup", and two VMs deployed the same day saw git-daemon 2.54 on s390x against 2.55 on amd64. For an ERP platform, that is not what should be offered by default. Leap 16.0 is numbered, stable, and publishes the three architectures that matter -- verified, x86_64, aarch64 and s390x all 200. It also unifies its tree: no separate /ports/, unlike Tumbleweed, whose equivalent paths 404 for Leap. Tumbleweed stays on offer, as a bellwether for breakage to come. Two things separate them in the scripts: the repository path, and "dup" -- which on Leap means CHANGING version, not updating. --- FR --- Le catalogue n'offrait qu'openSUSE Tumbleweed, une rolling sans numéro de version. Presque tous les incidents openSUSE de la série venaient de là : l'image cloud est un instantané en retard sur ses dépôts, d'où le « zypper dup » obligatoire, et deux VM déployées le même jour ont vu git-daemon 2.54 sur s390x contre 2.55 sur amd64. Pour une plateforme ERP, ce n'est pas ce qu'on veut proposer par défaut. Leap 16.0 est numérotée, stable, et publie les trois architectures qui comptent — vérifié, x86_64, aarch64 et s390x en 200. Elle unifie de plus son arbre : pas de /ports/ séparé, contrairement à Tumbleweed, dont les chemins équivalents rendent 404 pour Leap. Tumbleweed reste offerte, comme banc d'essai des ruptures à venir. Deux points les séparent dans les scripts : le chemin des dépôts, et « dup » — qui sur Leap sert à CHANGER de version, pas à mettre à jour. Assisted-by: Claude Opus 5 --- .claude/rules/08-deployment.md | 3 +- script/install/install_suse_dependency.sh | 23 ++++++++--- script/qemu/README.base.md | 26 ++++++++---- script/qemu/README.fr.md | 14 +++++-- script/qemu/README.md | 12 ++++-- script/qemu/deploy_qemu.py | 50 +++++++++++++++++------ script/todo/todo.py | 27 ++++++++++-- 7 files changed, 116 insertions(+), 39 deletions(-) diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md index a928e1a..e4ad023 100644 --- a/.claude/rules/08-deployment.md +++ b/.claude/rules/08-deployment.md @@ -7,7 +7,8 @@ - **DNS** : `script/deployment/update_dns_cloudflare.py` Plateformes supportées : Ubuntu 24.04 / 25.10 / 26.04, Linux Mint 22.3, -Debian 12, AlmaLinux 9+, Rocky Linux 9+, openSUSE Tumbleweed, Arch Linux, +Debian 12, AlmaLinux 9+, Rocky Linux 9+, openSUSE Leap 16 et Tumbleweed, +Arch Linux, macOS (mise ou pyenv), Windows (WSL/Docker). diff --git a/script/install/install_suse_dependency.sh b/script/install/install_suse_dependency.sh index e9c86cc..8170bf5 100755 --- a/script/install/install_suse_dependency.sh +++ b/script/install/install_suse_dependency.sh @@ -5,9 +5,13 @@ # Dépendances système ERPLibre pour la famille SUSE (zypper). Équivalent SUSE # de install_debian_dependency.sh et install_fedora_dependency.sh. # -# Visé : openSUSE Tumbleweed, la seule distribution du catalogue dont qpdf -# (12.3.2) dépasse déjà le seuil de pikepdf 10 — la compilation de qpdf, une -# demi-heure sous émulation s390x, ne s'y déclenche pas. +# Visé : openSUSE Leap 16 (le défaut, numéroté et stable) et Tumbleweed (la +# rolling, gardée comme banc d'essai). Les deux livrent un qpdf au-dessus du +# seuil de pikepdf 10 — la compilation de qpdf, une demi-heure sous émulation +# s390x, ne s'y déclenche pas. +# +# Ce qui les sépare dans ce script tient en deux points, tous deux marqués par +# SUSE_ROLLING : le chemin des dépôts, et « dup » contre « up ». . ./env_var.sh . ./script/install/lib_qpdf.sh @@ -148,8 +152,17 @@ sudo zypper --non-interactive refresh || true # réclamait un perl-Git bâti contre un perl-base plus ancien que celui de # l'image. zypper proposait alors trois solutions et ATTENDAIT un choix ; # « --non-interactive » prend le défaut, « c » = annuler, et tout s'arrêtait. -echo -e "\n---- Mise a jour complete (dup) ----" -${ZYP} dup ${ZYP_LIC} --allow-vendor-change || true +if [ "${SUSE_ROLLING}" = 1 ]; then + echo -e "\n---- Mise a jour complete (dup) ----" + ${ZYP} dup ${ZYP_LIC} --allow-vendor-change || true +else + # Sur Leap, « dup » sert à CHANGER de version, pas à mettre à jour : l'y + # appeler alignerait la machine sur ce que les dépôts contiennent, ce qui + # n'est pas ce qu'on veut d'une distribution justement choisie pour ne pas + # bouger. « up » suffit, l'image et ses dépôts étant de la même version. + echo -e "\n---- Mise a jour (up) ----" + ${ZYP} up ${ZYP_LIC} || true +fi #-------------------------------------------------- # Outils de compilation (build Python via pyenv, extensions Python) diff --git a/script/qemu/README.base.md b/script/qemu/README.base.md index 19fef19..b031ad7 100644 --- a/script/qemu/README.base.md +++ b/script/qemu/README.base.md @@ -182,15 +182,19 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth): | fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | | almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | -| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | +| opensuse | `16.0` (default), `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora builds s390x only for the current release, and on a separate tree (`fedora-secondary`) — hence the single version there. -`opensuse` is openSUSE Tumbleweed, the only entry whose qpdf (12.3.2) already -clears the pikepdf threshold: the half-hour qpdf build never runs there, which -matters under s390x emulation. +`opensuse` covers two distinct products, not two versions of one. Leap `16.0` +is numbered and stable (SLE base) and is the default. `tumbleweed` is the +rolling one, kept as a bellwether for breakage to come: its snapshot drift is +real, and it demands a full `zypper dup` before anything can be installed. + +Both ship a qpdf above the pikepdf threshold, so the half-hour qpdf build +never runs there — which matters under s390x emulation. Provide an explicit image path as a positional argument to override the automatic download location. @@ -212,15 +216,21 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) : | fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule | | almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | -| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | +| opensuse | `16.0` (défaut), `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora ne construit s390x que pour la version courante, et sur une arborescence à part (`fedora-secondary`) — d'où la version unique. -`opensuse` désigne openSUSE Tumbleweed, seule entrée dont qpdf (12.3.2) -dépasse déjà le seuil de pikepdf : la compilation de qpdf, une demi-heure, ne -s'y déclenche jamais — ce qui compte sous émulation s390x. +`opensuse` recouvre deux produits distincts, pas deux versions du même. Leap +`16.0` est numérotée et stable (base SLE) : c'est le défaut. `tumbleweed` est +la rolling, gardée comme banc d'essai des ruptures à venir — sa dérive +d'instantanés est réelle, et elle impose un `zypper dup` complet avant toute +installation. + +Les deux livrent un qpdf au-dessus du seuil de pikepdf : la compilation de +qpdf, une demi-heure, ne s'y déclenche jamais — ce qui compte sous émulation +s390x. Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. diff --git a/script/qemu/README.fr.md b/script/qemu/README.fr.md index 52c68ae..af7b826 100644 --- a/script/qemu/README.fr.md +++ b/script/qemu/README.fr.md @@ -101,15 +101,21 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) : | fedora | `41`, `42` (défaut), `43`, `44` | ✔ | ✔ | `43` seule | | almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | -| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | +| opensuse | `16.0` (défaut), `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora ne construit s390x que pour la version courante, et sur une arborescence à part (`fedora-secondary`) — d'où la version unique. -`opensuse` désigne openSUSE Tumbleweed, seule entrée dont qpdf (12.3.2) -dépasse déjà le seuil de pikepdf : la compilation de qpdf, une demi-heure, ne -s'y déclenche jamais — ce qui compte sous émulation s390x. +`opensuse` recouvre deux produits distincts, pas deux versions du même. Leap +`16.0` est numérotée et stable (base SLE) : c'est le défaut. `tumbleweed` est +la rolling, gardée comme banc d'essai des ruptures à venir — sa dérive +d'instantanés est réelle, et elle impose un `zypper dup` complet avant toute +installation. + +Les deux livrent un qpdf au-dessus du seuil de pikepdf : la compilation de +qpdf, une demi-heure, ne s'y déclenche jamais — ce qui compte sous émulation +s390x. Fournissez un chemin d'image en argument positionnel pour surcharger l'emplacement de téléchargement automatique. diff --git a/script/qemu/README.md b/script/qemu/README.md index cda760f..473a847 100644 --- a/script/qemu/README.md +++ b/script/qemu/README.md @@ -95,15 +95,19 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth): | fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | | almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | | rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | -| opensuse | `tumbleweed` | ✔ | ✔ | ✔ | +| opensuse | `16.0` (default), `tumbleweed` | ✔ | ✔ | ✔ | | arch | `latest` | ✔ | — | — | Fedora builds s390x only for the current release, and on a separate tree (`fedora-secondary`) — hence the single version there. -`opensuse` is openSUSE Tumbleweed, the only entry whose qpdf (12.3.2) already -clears the pikepdf threshold: the half-hour qpdf build never runs there, which -matters under s390x emulation. +`opensuse` covers two distinct products, not two versions of one. Leap `16.0` +is numbered and stable (SLE base) and is the default. `tumbleweed` is the +rolling one, kept as a bellwether for breakage to come: its snapshot drift is +real, and it demands a full `zypper dup` before anything can be installed. + +Both ship a qpdf above the pikepdf threshold, so the half-hour qpdf build +never runs there — which matters under s390x emulation. Provide an explicit image path as a positional argument to override the automatic download location. diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index c9da563..66e596d 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -113,10 +113,23 @@ ROCKY_VERSIONS: dict[str, tuple[str, str, int, str]] = { "9": ("9", "rocky9", 2048, "20G"), "10": ("10", "rocky10", 2048, "20G"), } -# openSUSE Tumbleweed : rolling, donc une seule « version ». C'est la seule -# distribution du catalogue dont qpdf (12.3.2) dépasse déjà le seuil de -# pikepdf 10 — aucune compilation de qpdf, ce qui change tout sous émulation. +# openSUSE, deux produits distincts et pas deux versions du même. +# Leap NUMÉROTÉ, base SLE, ~2 ans de support. C'est le défaut, et le +# seul des deux à proposer pour une installation qui doit durer. +# Tumbleweed ROLLING, sans numéro. Gardé comme banc d'essai des ruptures à +# venir, pas comme cible. Sa dérive d'instantanés est réelle : deux +# VM déployées le même jour ont vu git-daemon 2.54 sur s390x et +# 2.55 sur amd64, et l'image livrée est toujours en retard sur les +# dépôts, d'où le « zypper dup » obligatoire avant toute install. +# +# Les deux dépassent le seuil qpdf de pikepdf : aucune compilation de qpdf, ce +# qui change tout sous émulation s390x. +# +# osinfo : « opensuse16.0 » n'est pas encore dans osinfo-db, qui s'arrête à +# 15.6. osinfo_arg() replie sur le DERNIER id connu de la table — d'où l'ordre, +# Tumbleweed en second servant de repli à Leap. OPENSUSE_VERSIONS: dict[str, tuple[str, str, int, str]] = { + "16.0": ("16.0", "opensuse16.0", 2048, "20G"), "tumbleweed": ("tumbleweed", "opensusetumbleweed", 2048, "20G"), } # Arch est en rolling release : une seule « version » (latest). @@ -131,7 +144,7 @@ DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = { "fedora": (FEDORA_VERSIONS, "42"), "almalinux": (ALMALINUX_VERSIONS, "9"), "rocky": (ROCKY_VERSIONS, "10"), - "opensuse": (OPENSUSE_VERSIONS, "tumbleweed"), + "opensuse": (OPENSUSE_VERSIONS, "16.0"), "arch": (ARCH_VERSIONS, "latest"), } @@ -304,16 +317,25 @@ def image_candidates( f"Rocky-{version}-GenericCloud.latest.{a}.qcow2" ] if distro == "opensuse": - # Les architectures secondaires vivent sous /ports/, et zsystems DOUBLE - # l'architecture dans le nom du fichier — irrégularité vérifiée dans - # l'index, pas déduite : « .s390x-s390x-Cloud » contre « .x86_64-Cloud ». - port = {"s390x": "ports/zsystems/", "arm64": "ports/aarch64/"}.get( - arch, "" - ) + # zsystems DOUBLE l'architecture dans le nom du fichier — irrégularité + # vérifiée dans l'index, pas déduite : « .s390x-s390x-Cloud » contre + # « .x86_64-Cloud ». Elle vaut pour les DEUX produits. tag = f"{a}-{a}" if arch == "s390x" else a + if version == "tumbleweed": + # Tumbleweed sépare les architectures secondaires sous /ports/. + port = {"s390x": "ports/zsystems/", "arm64": "ports/aarch64/"}.get( + arch, "" + ) + return [ + f"{OPENSUSE_BASE}/{port}tumbleweed/appliances/" + f"openSUSE-Tumbleweed-Minimal-VM.{tag}-Cloud.qcow2" + ] + # Leap, lui, publie TOUTES les architectures dans un seul répertoire : + # les chemins /ports/ équivalents rendent 404. x86_64, aarch64 et s390x + # y sont côte à côte (relevé dans l'index de 16.0, les trois en 200). return [ - f"{OPENSUSE_BASE}/{port}tumbleweed/appliances/" - f"openSUSE-Tumbleweed-Minimal-VM.{tag}-Cloud.qcow2" + f"{OPENSUSE_BASE}/distribution/leap/{version}/appliances/" + f"Leap-{version}-Minimal-VM.{tag}-Cloud.qcow2" ] if distro == "arch": # Rolling release : image « latest » officielle (cloud-init inclus). @@ -375,7 +397,9 @@ def default_image_name(distro: str, code: str, arch: str, version: str) -> str: if distro == "arch": return f"arch-linux-{a}-cloudimg.qcow2" if distro == "opensuse": - return f"opensuse-tumbleweed-minimal-vm-{a}.qcow2" + if version == "tumbleweed": + return f"opensuse-tumbleweed-minimal-vm-{a}.qcow2" + return f"opensuse-leap-{version}-minimal-vm-{a}.qcow2" if distro in ("almalinux", "rocky"): # « latest » est MUTABLE : le nom de cache porte donc la majeure, et # une image déjà téléchargée sera réutilisée telle quelle. C'est voulu diff --git a/script/todo/todo.py b/script/todo/todo.py index 0c8039a..278915d 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1027,7 +1027,10 @@ class TODO: "fedora": (["41", "42", "43", "44"], "42"), "almalinux": (["9", "10"], "9"), "rocky": (["9", "10"], "10"), - "opensuse": (["tumbleweed"], "tumbleweed"), + # Leap 16.0 par défaut : numérotée et stable. Tumbleweed reste offerte, + # comme banc d'essai des ruptures à venir. Voir OPENSUSE_VERSIONS dans + # deploy_qemu.py, qui fait autorité sur le catalogue. + "opensuse": (["16.0", "tumbleweed"], "16.0"), "arch": (["latest"], "latest"), } @@ -4510,9 +4513,14 @@ class TODO: def _qemu_zypper_mirror_cmd(self): """Réécrit l'hôte des dépôts zypper vers un miroir plus proche.""" mirrors = " ".join(self._QEMU_ZYPPER_MIRRORS) + # Leap et Tumbleweed n'ont pas le même arbre de dépôts : la rolling + # isole les architectures secondaires sous /ports/, Leap 16 unifie tout + # et garde s390x dans l'arbre principal (les /ports/ y rendent 404). return ( - "zp=tumbleweed; " - '[ "$(uname -m)" = s390x ] && zp=ports/zsystems/tumbleweed; ' + ". /etc/os-release; " + 'case "$ID" in *tumbleweed*) zp=tumbleweed; ' + '[ "$(uname -m)" = s390x ] && zp=ports/zsystems/tumbleweed;; ' + '*) zp="distribution/leap/$VERSION_ID";; esac; ' f"for zm in {mirrors}; do " "if curl -fsS --max-time 20 -o /dev/null " '"$zm/$zp/repo/oss/repodata/repomd.xml"; then ' @@ -4722,8 +4730,19 @@ class TODO: # celui de l'image. zypper proposait alors trois solutions et # attendait un choix ; « --non-interactive » prend le défaut, # « c » = annuler, et l'installation s'arrêtait là. + # + # Sur Leap, « dup » sert à CHANGER de version : l'y appeler irait + # contre la raison même de la choisir. « up » y suffit, l'image et + # ses dépôts portant la même version. + # $ID est déjà posé par le bloc miroir juste au-dessus ; on le + # relit quand même, pour ne pas dépendre de l'ordre de deux + # méthodes qui s'ignorent. + ". /etc/os-release; " + 'case "$ID" in *tumbleweed*) ' "sudo zypper --non-interactive dup --auto-agree-with-licenses " - "--allow-vendor-change || true; " + "--allow-vendor-change || true;; " + "*) sudo zypper --non-interactive up " + "--auto-agree-with-licenses || true;; esac; " "sudo zypper --non-interactive install " "--auto-agree-with-licenses $PKGS; " "elif command -v yum >/dev/null 2>&1; then " From 834b1061ac7c817e8fecf2a9d3eb13254648b958 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 01:30:11 -0400 Subject: [PATCH 21/41] [ADD] tui qemu: customise one VM without touching the others MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three resource fields applied to the whole fleet. With a single machine needing 16 G, the other eight got it too. A scope selector settles it: all VMs, or the row targeted in the plan. The x1..x4 profile stays global, multiplying what each image asks for; under "selected VM" the fields are absolute, hence active even outside the custom profile. The F2 modal already existed but was undiscoverable, and its cursor was unusable: the table never held focus. It stays, the toggle now focuses the table, F4 returns a VM to the shared profile, and the plan marks customised rows with a ✎ -- without it, two rows with different resources have no explanation on screen. One defect found along the way: clear() reset the cursor to the top on every redraw, and the plan recomputes on every keystroke. You picked debian, typed the value, the table redrew, and the next entry landed on ubuntu with nothing to show for it. --- FR --- Les trois champs de ressources s'appliquaient à tout le parc. Une seule machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque image ; en portée « une seule » les champs valent en absolu, et sont donc actifs même hors profil personnalisé. La modale F2 existait déjà mais restait introuvable, et son curseur était inutilisable : le tableau n'avait jamais le focus. Elle demeure, la bascule lui donne le focus, F4 rend une VM au profil commun, et le plan marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux ressources différentes n'ont aucune explication à l'écran. Un défaut au passage : « clear() » ramenait le curseur en tête à chaque redessin, et le plan se recalcule à chaque frappe. On choisissait debian, on tapait la valeur, le tableau se redessinait, et la saisie suivante partait sur ubuntu sans que rien ne le montre. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 170 ++++++++++++++++++++++++++++++-- script/todo/todo_i18n.py | 20 ++++ 2 files changed, 181 insertions(+), 9 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index edc53bc..33087b8 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -370,6 +370,7 @@ def run_deploy_form(ctx, run_app: bool = True): BINDINGS = [ ("f5", "deploy", t("Deploy")), ("f2", "edit_vm", t("Edit VM")), + ("f4", "clear_vm", t("Reset VM")), ("f3", "preview", t("Preview")), ("f6", "select_all", t("All")), ("f7", "select_main", t("Main versions")), @@ -384,6 +385,15 @@ def run_deploy_form(ctx, run_app: bool = True): self.custom = {} self._free = {} self.overrides = {} + # « all » : les champs de ressources nourrissent le profil. + # « one » : ils écrivent une surcharge pour la ligne du plan. + self.scope = "all" + # Vrai pendant qu'on repositionne les widgets nous-mêmes : sans ce + # verrou, remettre une liste à vide déclencherait on_select_changed, + # qui réécrirait une surcharge — une boucle qui se nourrit seule. + self._syncing = False + # Dernier rang connu du curseur du plan. + self._last_row = 0 self.vms = [] self.rows = [] @@ -450,6 +460,18 @@ def run_deploy_form(ctx, run_app: bool = True): classes="freeval", disabled=True, ) + # Portée des TROIS champs ci-dessus. Le profil x1..x4 reste + # global par nature — il multiplie ce que demande chaque + # image. Ces valeurs-ci, elles, peuvent ne viser qu'une VM : + # une seule machine a besoin de 16 G, pas les huit autres. + # + # Le mode « une seule » rend les champs actifs même hors + # profil personnalisé : on y saisit une valeur absolue, pas + # un multiplicateur. + with RadioSet(id="f_scope"): + yield RadioButton(t("Apply to all VMs"), value=True) + yield RadioButton(t("Apply to the selected VM only")) + yield Static("", id="scopetarget") # Serveur par défaut : c'est ce que sert une image cloud, # et GNOME ajoute une à deux heures sur une architecture # émulée. Le plan annonce le surcoût disque. @@ -560,6 +582,7 @@ def run_deploy_form(ctx, run_app: bool = True): t("Status"), ) self._reload_catalog(first_load=True) + self._sync_res_fields() # -- catalogue et recalcul ------------------------------------- # def _entries(self): @@ -614,8 +637,16 @@ def run_deploy_form(ctx, run_app: bool = True): if self._desktop(): grow += desktop_disk self.rows = plan_rows(self.vms, domains, grow) + # Le plan doit MONTRER qu'une VM a été personnalisée : sans marque, + # deux lignes aux ressources différentes n'ont aucune explication à + # l'écran, et la surcharge est oubliée à la relecture. Le drapeau + # vit sur la ligne d'affichage, jamais sur la VM : celle-ci part + # telle quelle dans la spec, que la CLI produit à l'identique. + for row, entry in zip(self.rows, entries): + row["custom"] = bool(self.overrides.get(entry_key(entry))) self._render_plan() self._render_mise() + self.query_one("#scopetarget", Static).update(self._scope_label()) def _render_mise(self): """Grise le choix quand aucune VM retenue n'est servie par mise, @@ -671,6 +702,11 @@ def run_deploy_form(ctx, run_app: bool = True): def _render_plan(self): table = self.query_one("#plan", DataTable) + # « clear() » ramène le curseur en tête. Le plan étant recalculé à + # CHAQUE frappe, la ligne visée serait perdue entre deux saisies : + # on choisit debian, on tape 16384, le tableau se redessine, et la + # valeur suivante partirait sur ubuntu sans que rien ne le montre. + keep = table.cursor_row table.clear() # Le type ne concerne QUE les VM réellement créées : une VM déjà # définie n'est pas retouchée, lui afficher « GNOME » laisserait @@ -681,7 +717,7 @@ def run_deploy_form(ctx, run_app: bool = True): icon = {"new": "", "exists": "⏭ ", "orphan": "❌ "}[r["state"]] status = kind if r["state"] == "new" else f"{icon}{r['note']}" table.add_row( - vm["name"], + f"{vm['name']} ✎" if r.get("custom") else vm["name"], vm["distro"], vm["version"], vm["arch"], @@ -690,6 +726,10 @@ def run_deploy_form(ctx, run_app: bool = True): f"{r['disk_gb']}G", status, ) + if self.rows: + # Borné : décocher une entrée raccourcit la liste, et un rang + # devenu hors bornes ferait perdre la ligne visée pour de bon. + table.move_cursor(row=min(max(keep, 0), len(self.rows) - 1)) if not self.rows: # Rien de coché : un total à zéro n'apprend rien, on dit # plutôt comment remplir la liste. @@ -727,12 +767,94 @@ def run_deploy_form(ctx, run_app: bool = True): elif event.radio_set.id == "f_profile": index = event.radio_set.pressed_index self.profile = "custom" if index == 4 else str(index + 1) - custom = self.profile == "custom" - for field, (sel, inp) in RES_FIELDS.items(): - self.query_one(sel, Select).disabled = not custom - free = custom and self._free.get(field) - self._show_free(field, free) + self._sync_res_fields() self._recompute() + elif event.radio_set.id == "f_scope": + self.scope = ( + "one" if event.radio_set.pressed_index == 1 else "all" + ) + # Les champs repartent à vide : ils décrivent désormais une + # AUTRE cible, et y laisser la valeur précédente ferait croire + # qu'elle s'y applique déjà. + self._reset_res_widgets() + self._sync_res_fields() + if self.scope == "one": + # Sans focus, le curseur du tableau reste invisible et on + # modifierait une ligne qu'on ne voit pas désignée. + self.query_one("#plan", DataTable).focus() + self._recompute() + + # -- portée des ressources -------------------------------------- # + def _cursor_index(self): + """Rang de la ligne visée dans le plan, ou None.""" + index = self.query_one("#plan", DataTable).cursor_row + return index if 0 <= index < len(self.vms) else None + + def _cursor_key(self): + """Identité de catalogue de la ligne visée, ou None. C'est elle qui + indexe les surcharges, pas le rang : cocher une autre distro ne doit + pas déplacer une personnalisation d'une VM à l'autre.""" + index = self._cursor_index() + if index is None: + return None + return entry_key(self._selected_entries()[index]) + + def _scope_label(self): + """Ce que la saisie va toucher, dit en toutes lettres.""" + if self.scope != "one": + n = len(self.overrides) + if not n: + return "" + return f" ✎ {n} {t('VM(s) customised (F4 resets one)')}" + index = self._cursor_index() + if index is None: + return f" ⚠ {t('Pick a line in the plan (Tab, then arrows)')}" + vm = self.vms[index] + return ( + f" ✎ {vm['name']} — {vm['vcpus']} vCPU · " + f"{vm['ram']} Mo · {vm['disk']}" + ) + + def _sync_res_fields(self) -> None: + """Active les trois champs quand ils ont un sens, et redit la cible. + + Portée « toutes » : ils ne servent que le profil personnalisé, comme + avant. Portée « une seule » : ils valent toujours, puisqu'on y saisit + la valeur absolue d'une VM et non un multiplicateur.""" + live = self.scope == "one" or self.profile == "custom" + for field, (sel, _inp) in RES_FIELDS.items(): + self.query_one(sel, Select).disabled = not live + self._show_free(field, live and self._free.get(field)) + self.query_one("#scopetarget", Static).update(self._scope_label()) + + def _reset_res_widgets(self) -> None: + """Remet listes et saisies à vide SANS déclencher d'écriture.""" + self._syncing = True + try: + for field, (sel, inp) in RES_FIELDS.items(): + self.query_one(sel, Select).value = SELECT_NULL + self.query_one(inp, Input).value = "" + self._free[field] = False + self._show_free(field, False) + finally: + self._syncing = False + + def _set_resource(self, field, value) -> None: + """Écrit une ressource là où la portée le demande.""" + if self.scope != "one": + self.custom[field] = value + return + key = self._cursor_key() + if key is None: + return + if value in ("", 0): + # Saisie vidée ou invalide : on RETIRE la surcharge au lieu + # d'écrire un zéro, qui donnerait une VM à 0 vCPU. + self.overrides.get(key, {}).pop(field, None) + if not self.overrides.get(key): + self.overrides.pop(key, None) + else: + self.overrides.setdefault(key, {})[field] = value def _show_free(self, field, visible) -> None: """Montre ou cache la saisie libre d'une ressource.""" @@ -744,6 +866,8 @@ def run_deploy_form(ctx, run_app: bool = True): self._recompute() def on_select_changed(self, event) -> None: + if self._syncing: + return field = SELECT_TO_FIELD.get(event.select.id) if not field: return @@ -756,7 +880,7 @@ def run_deploy_form(ctx, run_app: bool = True): elif event.value is not SELECT_NULL: self._free[field] = False self._show_free(field, False) - self.custom[field] = event.value + self._set_resource(field, event.value) self._recompute() def _apply_free(self, field) -> None: @@ -764,16 +888,32 @@ def run_deploy_form(ctx, run_app: bool = True): profil retombe alors sur celle du catalogue.""" raw = self.query_one(RES_FIELDS[field][1], Input).value.strip() if field == "disk": - self.custom[field] = parse_disk(raw) or "" + self._set_resource(field, parse_disk(raw) or "") else: - self.custom[field] = positive_int(raw, 0) + self._set_resource(field, positive_int(raw, 0)) def on_input_changed(self, event) -> None: + if self._syncing: + return field = INPUT_TO_FIELD.get(event.input.id) if field: self._apply_free(field) self._recompute() + def on_data_table_row_highlighted(self, event) -> None: + """Changer de ligne change la cible : on repart de champs vides, + pour ne pas reporter par mégarde le réglage de la VM précédente. + + Le rang précédent est mémorisé plutôt que testé sur un verrou : + replacer le curseur après un redessin poste le même message, et + Textual le délivre APRÈS que le verrou soit retombé.""" + if event.cursor_row == self._last_row: + return + self._last_row = event.cursor_row + if self.scope == "one": + self._reset_res_widgets() + self._sync_res_fields() + def on_checkbox_changed(self, event) -> None: if event.checkbox.id == "f_install": self._recompute() # le disque annoncé inclut le +5 G ERPLibre @@ -810,6 +950,18 @@ def run_deploy_form(ctx, run_app: bool = True): self.push_screen(EditVMScreen(dict(self.vms[index])), apply) + def action_clear_vm(self) -> None: + """Rend la VM visée au profil commun. Sans cette sortie, une + personnalisation posée par erreur ne se défaisait qu'en rouvrant le + formulaire.""" + key = self._cursor_key() + if key is None or key not in self.overrides: + return + self.overrides.pop(key) + self._reset_res_widgets() + self._sync_res_fields() + self._recompute() + def _form_values(self): install = None if self.query_one("#f_install", Checkbox).value and profiles: diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 43019ce..c59292d 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2139,6 +2139,26 @@ TRANSLATIONS = { "fr": "Éditer la VM", "en": "Edit VM", }, + "Reset VM": { + "fr": "Réinit. VM", + "en": "Reset VM", + }, + "Apply to all VMs": { + "fr": "Appliquer à toutes les VM", + "en": "Apply to all VMs", + }, + "Apply to the selected VM only": { + "fr": "Appliquer à la VM sélectionnée seulement", + "en": "Apply to the selected VM only", + }, + "VM(s) customised (F4 resets one)": { + "fr": "VM personnalisée(s) (F4 en réinitialise une)", + "en": "VM(s) customised (F4 resets one)", + }, + "Pick a line in the plan (Tab, then arrows)": { + "fr": "Choisissez une ligne du plan (Tab, puis les flèches)", + "en": "Pick a line in the plan (Tab, then arrows)", + }, "Preview": { "fr": "Aperçu", "en": "Preview", From d6221ee48dd3ce596a4da8e238d3a933f471e1b5 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 02:46:41 -0400 Subject: [PATCH 22/41] [ADD] tui qemu: set every VM in place, type included MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VM type was global: the whole fleet as servers, or all of them GNOME. It now lives on the VM, all the way down -- the creation flag, and one remote command per machine at install time, where a single one served them all. The right pane is no longer a table but a row of widgets per VM: vCPU, RAM, disk and type, each with its usual values and a free entry. The left-hand fields become the shared default, which the screen now says. The scope selector and the F2 modal go away: given two ways to do the same thing, keep the visible one. Three traps came out of it, and the tests lock them. Widget ids carry a RANK, and the rank shifts when an entry is ticked, so an event from an already destroyed widget applied to the VM that took its place -- rows now carry a generation, marked BEFORE mounting, since mount_all empties the pending children and marking after it was a race. The x1..x4 profile no longer reached any VM. And a total of zero never said it had counted nothing. --- FR --- Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une commande distante par machine à l'installation, là où une seule les servait toutes. Le panneau de droite n'est plus un tableau mais une rangée de widgets par VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une saisie libre. Les champs de gauche deviennent le défaut commun, ce que l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent : entre deux façons de faire la même chose, on garde la visible. Trois pièges en sont sortis, et les tests les verrouillent. Les identifiants de widgets portent un RANG, et le rang se décale quand on coche une entrée : un événement émis par un widget déjà détruit s'appliquait à la VM qui avait pris sa place — les rangées portent maintenant une génération, marquée AVANT le montage, car mount_all vide les enfants en attente et marquer après était une course. Le profil x1..x4 n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il n'avait rien compté. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 668 +++++++++++++++++----------- script/todo/qemu_install_monitor.py | 8 +- script/todo/todo.py | 57 ++- script/todo/todo_i18n.py | 152 ++++++- 4 files changed, 604 insertions(+), 281 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 33087b8..81a6f10 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -21,6 +21,7 @@ préchargées par l'appelant et arrivent dans `ctx`. from __future__ import annotations import os +import re import time try: @@ -76,7 +77,33 @@ def positive_int(value, fallback): return number if number > 0 else fallback -def apply_profile(entries, profile, base_vcpus, host_cpu, custom=None): +def parse_ram(value): + """« 2048 », « 128G », « 1,5G » -> mébioctets, ou 0 si invalide. + + Les valeurs proposées s'affichent en G — « 2G », « 16G » — alors que la + saisie libre comptait en Mo. Taper « 128G », ce que l'affichage invite à + faire, rendait 0 : la surcharge était alors RETIRÉE et la VM revenait à la + valeur du catalogue, sans un mot. On accepte donc les deux écritures, un + nombre nu restant des mébioctets.""" + txt = str(value or "").strip().upper().replace(",", ".") + factor = 1 + if txt.endswith("GI"): + factor, txt = 1024, txt[:-2] + elif txt.endswith(("G", "T")): + factor = 1024 * (1024 if txt.endswith("T") else 1) + txt = txt[:-1] + elif txt.endswith(("M", "MI")): + txt = txt.rstrip("IM") + try: + mib = int(float(txt) * factor) + except ValueError: + return 0 + return mib if mib > 0 else 0 + + +def apply_profile( + entries, profile, base_vcpus, host_cpu, custom=None, desktop="" +): """Applique le profil de ressources aux entrées choisies. Reproduit à l'identique `TODO._qemu_prompt_resources` : un multiplicateur @@ -104,6 +131,10 @@ def apply_profile(entries, profile, base_vcpus, host_cpu, custom=None): "ram": ram, "disk": disk, "vcpus": vcpus, + # Type de VM (« » = serveur). Il vit sur la VM et non sur la + # spec entière depuis qu'il se choisit machine par machine ; + # `desktop` n'est plus que le défaut commun. + "desktop": desktop, } ) return out @@ -118,10 +149,12 @@ def apply_overrides(vms, entries, overrides): return vms -def build_vms(entries, profile, base_vcpus, host_cpu, custom, overrides): +def build_vms( + entries, profile, base_vcpus, host_cpu, custom, overrides, desktop="" +): """Catalogue choisi + profil + surcharges -> liste de VM de la spec.""" return apply_overrides( - apply_profile(entries, profile, base_vcpus, host_cpu, custom), + apply_profile(entries, profile, base_vcpus, host_cpu, custom, desktop), entries, overrides, ) @@ -214,13 +247,10 @@ def run_deploy_form(ctx, run_app: bool = True): from textual.containers import Horizontal, Vertical, VerticalScroll from textual.screen import ModalScreen from textual.widgets import ( - Button, Checkbox, - DataTable, Footer, Header, Input, - Label, RadioButton, RadioSet, Select, @@ -255,6 +285,9 @@ def run_deploy_form(ctx, run_app: bool = True): # Dernier choix de chaque liste de ressources : il ne porte pas de valeur, # il révèle la saisie libre placée juste dessous. FREE = "__free__" + # « Serveur » est un CHOIX, pas une absence de choix : lui donner « » le + # rendrait indistinguable de la sentinelle « rien de sélectionné ». + SERVER = "__server__" RES_FIELDS = { "vcpus": ("#f_vcpus", "#c_vcpus"), "ram": ("#f_ram", "#c_ram"), @@ -270,50 +303,6 @@ def run_deploy_form(ctx, run_app: bool = True): f"RAM≥{e['ram']}Mo {e['disk']}" ) - class EditVMScreen(ModalScreen): - """Réglages d'UNE VM. Un champ vide garde la valeur courante.""" - - BINDINGS = [("escape", "cancel", t("Cancel"))] - - def __init__(self, vm): - super().__init__() - self._vm = vm - - def compose(self) -> ComposeResult: - with Vertical(id="editbox"): - yield Static(f" {self._vm['name']}", id="edittitle") - yield Label(t("Name")) - yield Input(value=self._vm["name"], id="e_name") - yield Label(t("vCPU")) - yield Input(value=str(self._vm["vcpus"]), id="e_vcpus") - yield Label(t("RAM (MB)")) - yield Input(value=str(self._vm["ram"]), id="e_ram") - yield Label(t("Disk")) - yield Input(value=str(self._vm["disk"]), id="e_disk") - with Horizontal(id="editbtns"): - yield Button(t("Apply"), variant="primary", id="e_ok") - yield Button(t("Cancel"), id="e_cancel") - - def on_button_pressed(self, event) -> None: - if event.button.id != "e_ok": - self.dismiss(None) - return - out = {} - name = self.query_one("#e_name", Input).value.strip() - if name: - out["name"] = name - for field, wid in (("vcpus", "#e_vcpus"), ("ram", "#e_ram")): - raw = self.query_one(wid, Input).value.strip() - if raw.isdigit() and int(raw) > 0: - out[field] = int(raw) - disk = parse_disk(self.query_one("#e_disk", Input).value) - if disk: - out["disk"] = disk - self.dismiss(out) - - def action_cancel(self) -> None: - self.dismiss(None) - class PreviewScreen(ModalScreen): """Aperçu des commandes qui seraient lancées (aucune exécution).""" @@ -347,15 +336,33 @@ def run_deploy_form(ctx, run_app: bool = True): .grouptitle { color: $accent; text-style: bold; padding: 1 0 0 0; } SelectionList { height: 10; border: solid $panel; } RadioSet { height: auto; layout: horizontal; } - .freeval { display: none; } - #reslabel { color: $text-muted; } - EditVMScreen { align: center middle; } - #editbox { - width: 56; height: auto; padding: 1 2; - border: thick $accent; background: $surface; + .freeval { display: none; width: 9; } + /* « width: auto » sur la CARTE, pas seulement sur la rangée. Un + conteneur Textual vaut « width: 1fr » par défaut : la carte se bornait + donc au panneau, et c'est ELLE que « #plan » mesure pour sa largeur + virtuelle. La rangée avait beau être en « auto », son débordement était + coupé dans une carte qui ne grandissait pas, et la barre horizontale + n'apparaissait jamais. */ + .vmcard { + height: auto; width: auto; + border-bottom: solid $panel; padding: 0 1; } - #edittitle { color: $accent; text-style: bold; } - #editbtns { height: auto; padding-top: 1; } + /* Une VM figée se voit à la LIGNE, pas à une case perdue au bout : + c'est ce qui permet de balayer le plan et de savoir d'un coup ce qui + échappe au profil. */ + .vmcard.locked { background: $success 20%; } + .vmlock { width: 5; min-width: 5; } + /* La branche porte des noms longs (« 1.6.0 », « develop », + « feature/xyz ») : trop étroite, la liste les tronque et on ne sait + plus ce qu'on a choisi. */ + + + .vmcopy { width: 5; min-width: 5; } + .vmhead { height: 1; } + .vmrow { height: 3; align-vertical: middle; } + .vmrow Select { width: 15; } + .vmrow Input { width: 11; } + #reslabel { color: $text-muted; } PreviewScreen { align: center middle; } #prevbox { width: 90%; height: 70%; padding: 1 2; @@ -369,7 +376,6 @@ def run_deploy_form(ctx, run_app: bool = True): # par le champ de saisie qui a le focus. BINDINGS = [ ("f5", "deploy", t("Deploy")), - ("f2", "edit_vm", t("Edit VM")), ("f4", "clear_vm", t("Reset VM")), ("f3", "preview", t("Preview")), ("f6", "select_all", t("All")), @@ -385,15 +391,27 @@ def run_deploy_form(ctx, run_app: bool = True): self.custom = {} self._free = {} self.overrides = {} - # « all » : les champs de ressources nourrissent le profil. - # « one » : ils écrivent une surcharge pour la ligne du plan. - self.scope = "all" - # Vrai pendant qu'on repositionne les widgets nous-mêmes : sans ce - # verrou, remettre une liste à vide déclencherait on_select_changed, - # qui réécrirait une surcharge — une boucle qui se nourrit seule. + # Vrai pendant qu'on repositionne les widgets nous-mêmes : sans + # ce verrou, poser une valeur déclencherait on_select_changed, qui + # réécrirait une surcharge — une boucle qui se nourrit seule. self._syncing = False - # Dernier rang connu du curseur du plan. - self._last_row = 0 + # Jeu de VM actuellement monté dans le panneau droit. + self._shown_ids = () + # Génération du jeu de rangées monté. Les identifiants de widgets + # portent un RANG, et le rang se décale quand on coche ou décoche + # une entrée : un événement émis par un widget déjà détruit + # s'appliquerait alors à la VM qui a pris sa place. Chaque widget + # de rangée retient sa génération ; ceux d'une génération périmée + # sont ignorés. + self._gen = 0 + # VM dont les ressources sont FIGÉES, par identité de catalogue. + # Distinct des surcharges : une VM peut être modifiée sans être + # verrouillée, et le verrou fige TOUT, pas seulement ce qu'on a + # touché. + self.locked = set() + # {clé de base: nombre d'exemplaires EN PLUS du premier}. + self.copies = {} + self.rows = [] self.vms = [] self.rows = [] @@ -409,7 +427,10 @@ def run_deploy_form(ctx, run_app: bool = True): yield RadioButton(label, value=a == self.arch) yield Static(t("Catalog"), classes="grouptitle") yield SelectionList(id="f_catalog") - yield Static(t("Resources per VM"), classes="grouptitle") + yield Static( + t("Resources — applied to ALL VMs"), + classes="grouptitle", + ) with RadioSet(id="f_profile"): for label in ("x1", "x2", "x3", "x4"): yield RadioButton(label, value=label == "x1") @@ -437,12 +458,12 @@ def run_deploy_form(ctx, run_app: bool = True): for m in ctx["ram_presets"] ] + [(t("free value…"), FREE)], - prompt=t("RAM (MB)"), + prompt=t("RAM: 2048 or 8G"), id="f_ram", disabled=True, ) yield Input( - placeholder=t("RAM (MB)"), + placeholder=t("RAM: 2048 or 8G"), id="c_ram", classes="freeval", disabled=True, @@ -460,22 +481,15 @@ def run_deploy_form(ctx, run_app: bool = True): classes="freeval", disabled=True, ) - # Portée des TROIS champs ci-dessus. Le profil x1..x4 reste - # global par nature — il multiplie ce que demande chaque - # image. Ces valeurs-ci, elles, peuvent ne viser qu'une VM : - # une seule machine a besoin de 16 G, pas les huit autres. - # - # Le mode « une seule » rend les champs actifs même hors - # profil personnalisé : on y saisit une valeur absolue, pas - # un multiplicateur. - with RadioSet(id="f_scope"): - yield RadioButton(t("Apply to all VMs"), value=True) - yield RadioButton(t("Apply to the selected VM only")) - yield Static("", id="scopetarget") + yield Static( + f" {t('The profile and these fields change EVERY VM.')}" + f"\n {t('A VM edited on the right (marked) keeps its own.')}", + id="scopetarget", + ) # Serveur par défaut : c'est ce que sert une image cloud, # et GNOME ajoute une à deux heures sur une architecture # émulée. Le plan annonce le surcoût disque. - yield Static(t("VM type:"), classes="grouptitle") + yield Static(t("VM type (default):"), classes="grouptitle") with RadioSet(id="f_type"): yield RadioButton( t("Server (no graphical interface)"), @@ -563,26 +577,16 @@ def run_deploy_form(ctx, run_app: bool = True): id="f_par", ) with Vertical(id="right"): - yield DataTable(id="plan") + # Une liste de widgets, pas un tableau : chaque VM porte + # SES listes déroulantes, modifiables sur place. Un + # DataTable ne sait pas héberger de widget. + yield VerticalScroll(id="plan") yield Static("", id="totals") yield Footer() def on_mount(self) -> None: self.title = t("Deploy ERPLibre VM(s)!") - table = self.query_one("#plan", DataTable) - table.cursor_type = "row" - table.add_columns( - t("Name"), - t("Distro"), - t("Version"), - t("Arch"), - "vCPU", - "RAM", - t("Disk"), - t("Status"), - ) self._reload_catalog(first_load=True) - self._sync_res_fields() # -- catalogue et recalcul ------------------------------------- # def _entries(self): @@ -629,14 +633,19 @@ def run_deploy_form(ctx, run_app: bool = True): host_cpu, self.custom, self.overrides, + self._default_desktop(), ) # ERPLibre et GNOME pèsent chacun sur le disque, et se cumulent. grow = 0 if self.query_one("#f_install", Checkbox).value: grow += extra_disk - if self._desktop(): - grow += desktop_disk self.rows = plan_rows(self.vms, domains, grow) + # Le bureau pèse sur le disque de la VM QUI LE PORTE, et d'elle + # seule : un supplément commun mentait dès que les types + # différaient d'une machine à l'autre. + for row in self.rows: + if row["vm"].get("desktop"): + row["disk_gb"] += desktop_disk # Le plan doit MONTRER qu'une VM a été personnalisée : sans marque, # deux lignes aux ressources différentes n'ont aucune explication à # l'écran, et la surcharge est oubliée à la relecture. Le drapeau @@ -646,7 +655,6 @@ def run_deploy_form(ctx, run_app: bool = True): row["custom"] = bool(self.overrides.get(entry_key(entry))) self._render_plan() self._render_mise() - self.query_one("#scopetarget", Static).update(self._scope_label()) def _render_mise(self): """Grise le choix quand aucune VM retenue n'est servie par mise, @@ -685,51 +693,193 @@ def run_deploy_form(ctx, run_app: bool = True): def _mise_usable(self): return any(vm["arch"] in mise_arches for vm in self.vms) - def _desktop(self): + def _default_desktop(self): """« » pour un serveur, sinon la clé de la saveur choisie.""" + """Type de VM par défaut. Chaque rangée peut s'en écarter.""" index = self.query_one("#f_type", RadioSet).pressed_index if index is None or index < 1 or index > len(desktops): return "" return desktops[index - 1][0] - def _desktop_label(self): - """Libellé court du type de VM, pour le tableau et les totaux.""" - key = self._desktop() - if not key: - return t("server") - label = dict(desktops).get(key, key) - return f"🖥 {label}" + # -- panneau droit : une rangée de widgets par VM ---------------- # + def _row_ids(self): + """Identité du JEU de VM affiché. Reconstruire les widgets à chaque + frappe ferait perdre le focus en pleine saisie : on ne le fait que + si la liste elle-même a changé.""" + return tuple(entry_key(e) for e in self._selected_entries()) + + def _type_options(self): + return [(t("Server"), SERVER)] + [ + (label, key) for key, label in desktops + ] + + def _mount_rows(self) -> None: + """(Re)construit le panneau droit. + + Le verrou couvre TOUT le montage : poser « value= » sur un Select + fait émettre un Changed à Textual, que on_select_changed prenait + pour une saisie. Résultat mesuré — les trois champs de CHAQUE VM + recevaient une surcharge dès l'affichage, le profil x1..x4 ne + pouvait plus rien changer, et toutes les lignes portaient la + marque ✎. Il est relâché après le rafraîchissement, une fois ces + messages consommés.""" + self._syncing = True + self._gen += 1 + plan = self.query_one("#plan", VerticalScroll) + plan.remove_children() + widgets = [] + for i, r in enumerate(self.rows): + vm = r["vm"] + row = Horizontal( + Select( + [(str(c), c) for c in ctx["cpu_presets"]] + + [(t("free value…"), FREE)], + value=( + vm["vcpus"] + if vm["vcpus"] in ctx["cpu_presets"] + else SELECT_NULL + ), + prompt="vCPU", + id=f"v{i}_vcpus", + ), + Input( + value=( + "" + if vm["vcpus"] in ctx["cpu_presets"] + else str(vm["vcpus"]) + ), + placeholder="vCPU", + id=f"c{i}_vcpus", + classes="freeval", + ), + Select( + [(f"{m // 1024}G", m) for m in ctx["ram_presets"]] + + [(t("free value…"), FREE)], + value=( + vm["ram"] + if vm["ram"] in ctx["ram_presets"] + else SELECT_NULL + ), + prompt=t("RAM: 2048 or 8G"), + id=f"v{i}_ram", + ), + Input( + value=( + "" + if vm["ram"] in ctx["ram_presets"] + else str(vm["ram"]) + ), + placeholder=t("RAM: 2048 or 8G"), + id=f"c{i}_ram", + classes="freeval", + ), + Select( + [(d, d) for d in ctx["disk_presets"]] + + [(t("free value…"), FREE)], + value=( + vm["disk"] + if vm["disk"] in ctx["disk_presets"] + else SELECT_NULL + ), + prompt=t("Disk"), + id=f"v{i}_disk", + ), + Input( + value=( + "" + if vm["disk"] in ctx["disk_presets"] + else str(vm["disk"]) + ), + placeholder=t("Disk"), + id=f"c{i}_disk", + classes="freeval", + ), + Select( + self._type_options(), + value=vm.get("desktop") or SERVER, + allow_blank=False, + id=f"v{i}_type", + ), + classes="vmrow", + ) + widgets.append( + Vertical( + Static(self._row_head(i, r), id=f"h{i}"), + row, + classes="vmcard", + ) + ) + + # Marque de génération, posée sur CHAQUE widget de rangée. + # « walk_children() » ne voit RIEN avant le montage : les enfants + # passés au constructeur attendent dans « _pending_children ». + def mark(node): + node._el_gen = self._gen + for child in getattr(node, "_pending_children", None) or []: + mark(child) + + for card in widgets: + mark(card) + # Le montage vient APRÈS le marquage, jamais avant : « mount_all » + # consomme « _pending_children » et le vide. Marquer ensuite était + # une COURSE — gagnée sur une machine, perdue sur une autre. Perdue, + # les listes des rangées n'avaient plus de génération, TOUS les + # changements par VM étaient rejetés en silence, et seuls les + # réglages globaux semblaient agir. + if widgets: + plan.mount_all(widgets) + self._shown_ids = self._row_ids() + # Les saisies libres ne se révèlent qu'après le montage : leur + # style ne peut pas être touché avant qu'elles existent. + self.call_after_refresh(self._after_mount_rows) + + def _after_mount_rows(self) -> None: + self._sync_free_inputs() + self._syncing = False + + def _sync_free_inputs(self) -> None: + for i, r in enumerate(self.rows): + vm = r["vm"] + for field, presets in ( + ("vcpus", ctx["cpu_presets"]), + ("ram", ctx["ram_presets"]), + ("disk", ctx["disk_presets"]), + ): + try: + widget = self.query_one(f"#c{i}_{field}", Input) + except Exception: + continue + free = vm[field] not in presets + widget.display = free + widget.disabled = not free + + def _row_head(self, index, row): + """Ligne de titre d'une VM : nom, origine, état, marque de + personnalisation. Sans elle, deux rangées aux réglages différents + n'ont aucune explication à l'écran.""" + vm = row["vm"] + icon = {"new": "", "exists": "⏭ ", "orphan": "❌ "}[row["state"]] + state = "" if row["state"] == "new" else f" {icon}{row['note']}" + mark = " ✎" if row.get("custom") else "" + return ( + f"[b]{vm['name']}[/b] {vm['distro']} {vm['version']} " + f"[{vm['arch']}] {row['disk_gb']}G{state}{mark}" + ) def _render_plan(self): - table = self.query_one("#plan", DataTable) - # « clear() » ramène le curseur en tête. Le plan étant recalculé à - # CHAQUE frappe, la ligne visée serait perdue entre deux saisies : - # on choisit debian, on tape 16384, le tableau se redessine, et la - # valeur suivante partirait sur ubuntu sans que rien ne le montre. - keep = table.cursor_row - table.clear() - # Le type ne concerne QUE les VM réellement créées : une VM déjà - # définie n'est pas retouchée, lui afficher « GNOME » laisserait - # croire qu'on va lui poser un bureau. - kind = self._desktop_label() - for r in self.rows: - vm = r["vm"] - icon = {"new": "", "exists": "⏭ ", "orphan": "❌ "}[r["state"]] - status = kind if r["state"] == "new" else f"{icon}{r['note']}" - table.add_row( - f"{vm['name']} ✎" if r.get("custom") else vm["name"], - vm["distro"], - vm["version"], - vm["arch"], - str(vm["vcpus"]), - f"{vm['ram']}Mo", - f"{r['disk_gb']}G", - status, - ) - if self.rows: - # Borné : décocher une entrée raccourcit la liste, et un rang - # devenu hors bornes ferait perdre la ligne visée pour de bon. - table.move_cursor(row=min(max(keep, 0), len(self.rows) - 1)) + # Le JEU de VM a-t-il changé ? Si oui on remonte les widgets, sinon + # on se contente des titres : remonter à chaque frappe volerait le + # focus au champ en cours de saisie. + if self._row_ids() != self._shown_ids: + self._mount_rows() + else: + for i, r in enumerate(self.rows): + try: + self.query_one(f"#h{i}", Static).update( + self._row_head(i, r) + ) + except Exception: + pass if not self.rows: # Rien de coché : un total à zéro n'apprend rien, on dit # plutôt comment remplir la liste. @@ -750,9 +900,20 @@ def run_deploy_form(ctx, run_app: bool = True): if dupes else "" ) + # Une VM DÉJÀ DÉFINIE n'est pas recréée : elle ne consomme rien + # de neuf, donc plan_totals l'écarte. Mais un total à zéro sans + # explication se lit comme un bogue — on a cru que les réglages + # par VM n'avaient aucun effet, alors qu'ils portaient sur une + # machine qui ne sera pas créée. + skipped = sum(1 for r in self.rows if r["state"] == "exists") + skip_txt = ( + f" ({skipped} {t('already defined, not counted')})" + if skipped + else "" + ) self.query_one("#totals", Static).update( f" {n} {t('VMs')} · {cpus} vCPU · {ram} Mo · ~{disk} G" - f" · {self._desktop_label()}{warn}{dup_txt}" + f"{skip_txt}{warn}{dup_txt}" ) # -- réactions aux champs -------------------------------------- # @@ -767,87 +928,38 @@ def run_deploy_form(ctx, run_app: bool = True): elif event.radio_set.id == "f_profile": index = event.radio_set.pressed_index self.profile = "custom" if index == 4 else str(index + 1) - self._sync_res_fields() - self._recompute() - elif event.radio_set.id == "f_scope": - self.scope = ( - "one" if event.radio_set.pressed_index == 1 else "all" - ) - # Les champs repartent à vide : ils décrivent désormais une - # AUTRE cible, et y laisser la valeur précédente ferait croire - # qu'elle s'y applique déjà. - self._reset_res_widgets() - self._sync_res_fields() - if self.scope == "one": - # Sans focus, le curseur du tableau reste invisible et on - # modifierait une ligne qu'on ne voit pas désignée. - self.query_one("#plan", DataTable).focus() + custom = self.profile == "custom" + for field, (sel, _inp) in RES_FIELDS.items(): + self.query_one(sel, Select).disabled = not custom + self._show_free(field, custom and self._free.get(field)) self._recompute() - # -- portée des ressources -------------------------------------- # - def _cursor_index(self): - """Rang de la ligne visée dans le plan, ou None.""" - index = self.query_one("#plan", DataTable).cursor_row - return index if 0 <= index < len(self.vms) else None + # -- rangées du panneau droit ------------------------------- # + def _is_current(self, widget): + """Le widget appartient-il au jeu de rangées ACTUEL ?""" + return getattr(widget, "_el_gen", None) == self._gen - def _cursor_key(self): - """Identité de catalogue de la ligne visée, ou None. C'est elle qui - indexe les surcharges, pas le rang : cocher une autre distro ne doit - pas déplacer une personnalisation d'une VM à l'autre.""" - index = self._cursor_index() - if index is None: + def _focused_row(self): + """Rang de la VM dont un widget a le focus, ou None. C'est la seule + désignation qui ait un sens ici : il n'y a plus de curseur unique, + chaque rangée est éditable directement.""" + wid = getattr(self.focused, "id", "") or "" + match = re.match(r"[vch](\d+)(?:_|$)", wid) + if not match: return None - return entry_key(self._selected_entries()[index]) + index = int(match.group(1)) + return index if 0 <= index < len(self.rows) else None - def _scope_label(self): - """Ce que la saisie va toucher, dit en toutes lettres.""" - if self.scope != "one": - n = len(self.overrides) - if not n: - return "" - return f" ✎ {n} {t('VM(s) customised (F4 resets one)')}" - index = self._cursor_index() - if index is None: - return f" ⚠ {t('Pick a line in the plan (Tab, then arrows)')}" - vm = self.vms[index] - return ( - f" ✎ {vm['name']} — {vm['vcpus']} vCPU · " - f"{vm['ram']} Mo · {vm['disk']}" - ) + def _row_key(self, index): + entries = self._selected_entries() + return entry_key(entries[index]) if index < len(entries) else None - def _sync_res_fields(self) -> None: - """Active les trois champs quand ils ont un sens, et redit la cible. - - Portée « toutes » : ils ne servent que le profil personnalisé, comme - avant. Portée « une seule » : ils valent toujours, puisqu'on y saisit - la valeur absolue d'une VM et non un multiplicateur.""" - live = self.scope == "one" or self.profile == "custom" - for field, (sel, _inp) in RES_FIELDS.items(): - self.query_one(sel, Select).disabled = not live - self._show_free(field, live and self._free.get(field)) - self.query_one("#scopetarget", Static).update(self._scope_label()) - - def _reset_res_widgets(self) -> None: - """Remet listes et saisies à vide SANS déclencher d'écriture.""" - self._syncing = True - try: - for field, (sel, inp) in RES_FIELDS.items(): - self.query_one(sel, Select).value = SELECT_NULL - self.query_one(inp, Input).value = "" - self._free[field] = False - self._show_free(field, False) - finally: - self._syncing = False - - def _set_resource(self, field, value) -> None: - """Écrit une ressource là où la portée le demande.""" - if self.scope != "one": - self.custom[field] = value - return - key = self._cursor_key() + def _set_override(self, index, field, value) -> None: + """Écrit — ou retire — la surcharge d'UNE VM.""" + key = self._row_key(index) if key is None: return - if value in ("", 0): + if value in ("", 0, None): # Saisie vidée ou invalide : on RETIRE la surcharge au lieu # d'écrire un zéro, qui donnerait une VM à 0 vCPU. self.overrides.get(key, {}).pop(field, None) @@ -856,6 +968,21 @@ def run_deploy_form(ctx, run_app: bool = True): else: self.overrides.setdefault(key, {})[field] = value + def _row_free(self, index, field, visible) -> None: + widget = self.query_one(f"#c{index}_{field}", Input) + widget.display = bool(visible) + widget.disabled = not visible + if visible: + widget.focus() + + def _read_row_free(self, index, field): + raw = self.query_one(f"#c{index}_{field}", Input).value.strip() + if field == "disk": + return parse_disk(raw) or "" + if field == "ram": + return parse_ram(raw) + return positive_int(raw, 0) + def _show_free(self, field, visible) -> None: """Montre ou cache la saisie libre d'une ressource.""" widget = self.query_one(RES_FIELDS[field][1], Input) @@ -868,6 +995,59 @@ def run_deploy_form(ctx, run_app: bool = True): def on_select_changed(self, event) -> None: if self._syncing: return + wid = event.select.id or "" + row = re.match(r"v(\d+)_(vcpus|ram|disk|type|branch|prof)$", wid) + if row and not self._is_current(event.select): + # Widget d'une génération périmée : son rang ne désigne plus + # la même VM. L'appliquer écraserait le réglage d'une voisine. + return + if row: + index, field = int(row.group(1)), row.group(2) + if index >= len(self.rows): + return + # Poser « value= » au montage fait émettre un Changed que + # Textual délivre APRÈS coup : un verrou temporel ne l'attrape + # pas — mesuré, les trois champs de chaque VM se retrouvaient + # surchargés dès l'affichage et le profil x1..x4 devenait + # inopérant. On compare donc à ce que le modèle dit déjà : une + # valeur identique n'est pas une saisie, c'est l'écho. + # + # Cas limite assumé : choisir explicitement la valeur que le + # profil donne déjà n'enregistre pas de surcharge. La VM + # suivra donc le profil s'il change — ce qui est aussi le plus + # attendu quand on n'a rien changé de visible. + vm_now = self.rows[index]["vm"] + if field == "type": + new_desk = "" if event.value == SERVER else event.value + if new_desk == (vm_now.get("desktop") or ""): + return + elif ( + event.value is not FREE and event.value is not SELECT_NULL + ): + if event.value == vm_now.get(field): + return + if field == "type": + self._set_override( + index, + "desktop", + "" if event.value == SERVER else event.value, + ) + # « Serveur » est un choix légitime, pas un retrait : on le + # note explicitement pour qu'il tienne face au défaut. + if event.value == SERVER: + key = self._row_key(index) + if key is not None: + self.overrides.setdefault(key, {})["desktop"] = "" + elif event.value is FREE: + self._row_free(index, field, True) + self._set_override( + index, field, self._read_row_free(index, field) + ) + elif event.value is not SELECT_NULL: + self._row_free(index, field, False) + self._set_override(index, field, event.value) + self._recompute() + return field = SELECT_TO_FIELD.get(event.select.id) if not field: return @@ -880,7 +1060,7 @@ def run_deploy_form(ctx, run_app: bool = True): elif event.value is not SELECT_NULL: self._free[field] = False self._show_free(field, False) - self._set_resource(field, event.value) + self.custom[field] = event.value self._recompute() def _apply_free(self, field) -> None: @@ -888,32 +1068,31 @@ def run_deploy_form(ctx, run_app: bool = True): profil retombe alors sur celle du catalogue.""" raw = self.query_one(RES_FIELDS[field][1], Input).value.strip() if field == "disk": - self._set_resource(field, parse_disk(raw) or "") + self.custom[field] = parse_disk(raw) or "" + elif field == "ram": + self.custom[field] = parse_ram(raw) else: - self._set_resource(field, positive_int(raw, 0)) + self.custom[field] = positive_int(raw, 0) def on_input_changed(self, event) -> None: if self._syncing: return + wid = event.input.id or "" + row = re.match(r"c(\d+)_(vcpus|ram|disk)$", wid) + if row and not self._is_current(event.input): + return + if row: + index, field = int(row.group(1)), row.group(2) + self._set_override( + index, field, self._read_row_free(index, field) + ) + self._recompute() + return field = INPUT_TO_FIELD.get(event.input.id) if field: self._apply_free(field) self._recompute() - def on_data_table_row_highlighted(self, event) -> None: - """Changer de ligne change la cible : on repart de champs vides, - pour ne pas reporter par mégarde le réglage de la VM précédente. - - Le rang précédent est mémorisé plutôt que testé sur un verrou : - replacer le curseur après un redessin poste le même message, et - Textual le délivre APRÈS que le verrou soit retombé.""" - if event.cursor_row == self._last_row: - return - self._last_row = event.cursor_row - if self.scope == "one": - self._reset_res_widgets() - self._sync_res_fields() - def on_checkbox_changed(self, event) -> None: if event.checkbox.id == "f_install": self._recompute() # le disque annoncé inclut le +5 G ERPLibre @@ -935,32 +1114,21 @@ def run_deploy_form(ctx, run_app: bool = True): if e.get("default"): widget.select(i) - def action_edit_vm(self) -> None: - table = self.query_one("#plan", DataTable) - index = table.cursor_row - if not (0 <= index < len(self.vms)): - return - entries = self._selected_entries() - key = entry_key(entries[index]) - - def apply(changes): - if changes: - self.overrides.setdefault(key, {}).update(changes) - self._recompute() - - self.push_screen(EditVMScreen(dict(self.vms[index])), apply) - def action_clear_vm(self) -> None: - """Rend la VM visée au profil commun. Sans cette sortie, une - personnalisation posée par erreur ne se défaisait qu'en rouvrant le - formulaire.""" - key = self._cursor_key() + """Rend au profil commun la VM dont un widget a le focus. Sans + cette sortie, un réglage posé par erreur ne se défaisait qu'en + rouvrant le formulaire.""" + index = self._focused_row() + if index is None: + return + key = self._row_key(index) if key is None or key not in self.overrides: return self.overrides.pop(key) - self._reset_res_widgets() - self._sync_res_fields() + # Les widgets de la rangée portent encore l'ancienne valeur : on + # les remonte pour qu'ils disent la vérité. self._recompute() + self._mount_rows() def _form_values(self): install = None @@ -987,7 +1155,7 @@ def run_deploy_form(ctx, run_app: bool = True): "timezone": self.query_one("#f_tz", Input).value.strip() or ctx.get("timezone") or "", - "desktop": self._desktop(), + "desktop": self._default_desktop(), "python_provider": self._python_provider(), "install": install, "add_ssh_config": self.query_one("#f_sshcfg", Checkbox).value, diff --git a/script/todo/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index 2558bf0..dc6664f 100644 --- a/script/todo/qemu_install_monitor.py +++ b/script/todo/qemu_install_monitor.py @@ -249,7 +249,13 @@ def launch_installs(vms: list[dict], branch: str, remote_cmd: str) -> str: # En-tête d'emblée (date/distro/version/arch) : le log n'est jamais # vide, l'utilisateur voit tout de suite QUOI s'installe. Path(log_path).write_text(_log_header(vm, branch, when)) - _launch_one(vm["ip"], remote_cmd, log_path, vm["name"]) + # Une VM peut porter SA commande : depuis que le type de VM (serveur + # ou bureau) se choisit machine par machine, le script distant n'est + # plus le même pour toutes. `remote_cmd` reste le défaut, ce qui laisse + # intacts les appelants qui n'en fournissent qu'une. + _launch_one( + vm["ip"], vm.get("remote_cmd") or remote_cmd, log_path, vm["name"] + ) entries.append( { "name": vm["name"], diff --git a/script/todo/todo.py b/script/todo/todo.py index 278915d..5490ca9 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4797,8 +4797,17 @@ class TODO: run_monitor, ) + # `desktop` accepte une SAVEUR unique (toutes les VM) ou un dict + # {nom: saveur} depuis que le type se choisit machine par machine. La + # commande distante en dépend, donc elle se construit par VM ; celle-ci + # reste le défaut pour les noms absents du dict. + desk_map = desktop if isinstance(desktop, dict) else {} remote = self._qemu_erplibre_remote_cmd( - branch, final_cmd, prod, desktop, python_provider + branch, + final_cmd, + prod, + "" if desk_map else desktop, + python_provider, ) try: mod = self._qemu_import_module() @@ -4815,15 +4824,22 @@ class TODO: if mod else (None, None, None) ) - vms.append( - { - "name": name, - "ip": ip, - "distro": d, - "version": v, - "arch": a, - } - ) + entry = { + "name": name, + "ip": ip, + "distro": d, + "version": v, + "arch": a, + } + if desk_map: + entry["remote_cmd"] = self._qemu_erplibre_remote_cmd( + branch, + final_cmd, + prod, + desk_map.get(name, ""), + python_provider, + ) + vms.append(entry) else: print(f" {name}: {t('no IP, skipped.')}") if not vms: @@ -5361,7 +5377,9 @@ class TODO: dry_run=dry_run, timezone=spec.get("timezone"), locale=spec.get("locale"), - desktop=bool(spec.get("desktop")), + # Le type suit la VM. Repli sur la valeur de spec pour la CLI, + # qui ne pose la question qu'une fois pour tout le parc. + desktop=bool(vm.get("desktop", spec.get("desktop"))), ) # ---------------------------------------------------------------- # @@ -6121,7 +6139,18 @@ class TODO: deployed = list(spec.get("existing") or []) install = spec.get("install") install_branch = install["branch"] if install else None - desktop = spec.get("desktop") or "" + # Le type de VM est choisi machine par machine dans la TUI ; la CLI n'en + # pose qu'un pour tout le parc. On ramene les deux a la meme carte, et + # `desktop` reste la reponse a « faut-il installer un bureau quelque + # part ? », qui declenche la phase d'installation. + desktop_default = spec.get("desktop") or "" + desktop_map = { + vm["name"]: (vm.get("desktop", desktop_default) or "") + for vm in pending + } + for _name in deployed: + desktop_map.setdefault(_name, desktop_default) + desktop = next((d for d in desktop_map.values() if d), "") python_provider = spec.get("python_provider") or "" ssh_key = spec.get("ssh_key") add_ssh_config = spec["add_ssh_config"] @@ -6196,7 +6225,7 @@ class TODO: ip_map, install["cmd"] if install else None, install["prod"] if install else False, - desktop=desktop, + desktop=desktop_map, python_provider=python_provider, ) else: @@ -6212,7 +6241,7 @@ class TODO: ip_map.get(name), install["cmd"], install["prod"], - desktop=desktop, + desktop=desktop_map.get(name, ""), python_provider=python_provider, ) diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index c59292d..a716ed4 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2139,26 +2139,146 @@ TRANSLATIONS = { "fr": "Éditer la VM", "en": "Edit VM", }, + "Server": { + "fr": "Serveur", + "en": "Server", + }, + "VM type (default):": { + "fr": "Type de VM (défaut) :", + "en": "VM type (default):", + }, + "These values are the default for every VM;": { + "fr": "Ces valeurs sont le défaut de chaque VM ;", + "en": "These values are the default for every VM;", + }, + "adjust any of them per VM on the right.": { + "fr": "ajustez-les VM par VM dans la vue de droite.", + "en": "adjust any of them per VM on the right.", + }, + "Application store:": { + "fr": "Magasin d'applications :", + "en": "Application store:", + }, + "Application store (graphical Ubuntu VMs):": { + "fr": "Magasin d'applications (VM Ubuntu graphiques) :", + "en": "Application store (graphical Ubuntu VMs):", + }, + "deb only (epiphany-browser)": { + "fr": "deb uniquement (epiphany-browser)", + "en": "deb only (epiphany-browser)", + }, + "Flatpak tooling, no Flathub": { + "fr": "outillage Flatpak, sans Flathub", + "en": "Flatpak tooling, no Flathub", + }, + "snap (Ubuntu default, Firefox)": { + "fr": "snap (défaut Ubuntu, Firefox)", + "en": "snap (Ubuntu default, Firefox)", + }, + "snap needs the store; slow under emulation.": { + "fr": "snap exige le store ; lent sous émulation.", + "en": "snap needs the store; slow under emulation.", + }, + "No graphical VM on a snap-based distro.": { + "fr": "Aucune VM graphique sur une distribution à snap.", + "en": "No graphical VM on a snap-based distro.", + }, + "Resources — applied to ALL VMs": { + "fr": "Ressources — appliquées à TOUTES les VM", + "en": "Resources — applied to ALL VMs", + }, + "The profile and these fields change EVERY VM.": { + "fr": "Le profil et ces champs changent TOUTES les VM.", + "en": "The profile and these fields change EVERY VM.", + }, + "A VM edited on the right (marked) keeps its own.": { + "fr": "Une VM modifiée à droite (marquée ✎) garde les siennes.", + "en": "A VM edited on the right (marked) keeps its own.", + }, + "Rename the VM": { + "fr": "Renommer la VM", + "en": "Rename the VM", + }, + "Rename": { + "fr": "Renommer", + "en": "Rename", + }, + "Empty = back to the automatic name:": { + "fr": "Vide = revenir au nom automatique :", + "en": "Empty = back to the automatic name:", + }, + "Invalid name: letters, digits, hyphens.": { + "fr": "Nom invalide : lettres, chiffres et traits d'union.", + "en": "Invalid name: letters, digits, hyphens.", + }, + "default; a VM may differ, see its line": { + "fr": "défaut ; une VM peut s'en écarter, voir sa ligne", + "en": "default; a VM may differ, see its line", + }, + "varies, see each line": { + "fr": "varie, voir chaque ligne", + "en": "varies, see each line", + }, + "From your workstation:": { + "fr": "Depuis votre poste :", + "en": "From your workstation:", + }, + "then point your client at": { + "fr": "puis pointez votre client sur", + "en": "then point your client at", + }, + "Remote desktop tunnel (VNC/RDP through SSH)": { + "fr": "🖥 Tunnel bureau distant (VNC/RDP par SSH)", + "en": "🖥 Remote desktop tunnel (VNC/RDP through SSH)", + }, + "Remote desktop tunnel": { + "fr": "Tunnel vers le bureau distant", + "en": "Remote desktop tunnel", + }, + "Which VM?": {"fr": "Quelle VM ?", "en": "Which VM?"}, + "No IP for this VM; is it running?": { + "fr": "Pas d'IP pour cette VM ; tourne-t-elle ?", + "en": "No IP for this VM; is it running?", + }, + "Run this on YOUR workstation:": { + "fr": "À lancer sur VOTRE poste :", + "en": "Run this on YOUR workstation:", + }, + "(through the ProxyJump already in ~/.ssh/config)": { + "fr": "(par le ProxyJump déjà dans ~/.ssh/config)", + "en": "(through the ProxyJump already in ~/.ssh/config)", + }, + "No ~/.ssh/config entry; see SSH configuration.": { + "fr": "Aucune entrée ~/.ssh/config ; voir Configuration SSH.", + "en": "No ~/.ssh/config entry; see SSH configuration.", + }, + "Not in an SSH session: check the host address.": { + "fr": "Hors session SSH : vérifiez l'adresse de l'hôte.", + "en": "Not in an SSH session: check the host address.", + }, + "The tunnel stays open as long as that ssh runs.": { + "fr": "Le tunnel reste ouvert tant que ce ssh tourne.", + "en": "The tunnel stays open as long as that ssh runs.", + }, + "No VM defined.": {"fr": "Aucune VM définie.", "en": "No VM defined."}, + "No host in ~/.ssh/config and no local VM.": { + "fr": "Aucun hôte dans ~/.ssh/config et aucune VM locale.", + "en": "No host in ~/.ssh/config and no local VM.", + }, + "local VM": {"fr": "VM locale", "en": "local VM"}, + "Remote desktop kind:": { + "fr": "Type de bureau distant :", + "en": "Remote desktop kind:", + }, + "already defined, not counted": { + "fr": "déjà définie(s), non comptée(s)", + "en": "already defined, not counted", + }, + "RAM: 2048 or 8G": {"fr": "RAM : 2048 ou 8G", "en": "RAM: 2048 or 8G"}, "Reset VM": { "fr": "Réinit. VM", "en": "Reset VM", }, - "Apply to all VMs": { - "fr": "Appliquer à toutes les VM", - "en": "Apply to all VMs", - }, - "Apply to the selected VM only": { - "fr": "Appliquer à la VM sélectionnée seulement", - "en": "Apply to the selected VM only", - }, - "VM(s) customised (F4 resets one)": { - "fr": "VM personnalisée(s) (F4 en réinitialise une)", - "en": "VM(s) customised (F4 resets one)", - }, - "Pick a line in the plan (Tab, then arrows)": { - "fr": "Choisissez une ligne du plan (Tab, puis les flèches)", - "en": "Pick a line in the plan (Tab, then arrows)", - }, "Preview": { "fr": "Aperçu", "en": "Preview", From 62f3e62e83b9985aa5e481c3f903183e5d7b0e13 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 02:56:18 -0400 Subject: [PATCH 23/41] [FIX] install s390x: the compiler was killed for lack of memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit matplotlib dies on "c++: fatal error: Killed signal terminated program cc1plus". "Killed" is a SIGKILL: the kernel's OOM killer, and nothing in the message names memory. A single matplotlib source file asks cc1plus for up to 2.5 GiB. Two causes compound. The VM is small -- the catalogue starts at 1 or 2 GiB -- and every build backend launches nproc compilations in parallel, each with its own cc1plus. Six cores exhaust 8 GiB. Hence two answers. A swap file tops memory up to 8 GiB, never taking more than half the free disk nor touching fstab -- a declared and missing swap degrades boot. And parallelism is bounded by actual memory, 2 GiB per task. ninja has no parallelism environment variable, but meson-python reads "NINJA", the executable PATH -- verified in mesonpy 0.20. We point it at a wrapper that adds the -j. That is what saves matplotlib; MAKEFLAGS and CMAKE_BUILD_PARALLEL_LEVEL cover the rest. --- FR --- matplotlib s'arrête sur « c++: fatal error: Killed signal terminated program cc1plus ». « Killed » est un SIGKILL : c'est le tueur du noyau, et rien dans le message ne nomme la mémoire. Un seul fichier de matplotlib demande jusqu'à 2,5 Gio à cc1plus. Deux causes se cumulent. La VM est petite — le catalogue démarre à 1 ou 2 Gio — et chaque moteur de build lance nproc compilations en parallèle, chacune avec son cc1plus. Six cœurs épuisent 8 Gio. D'où deux réponses. Un fichier d'échange complète la mémoire jusqu'à 8 Gio, sans jamais prendre plus de la moitié du disque libre ni toucher à fstab — un swap déclaré et disparu dégrade le démarrage. Et le parallélisme est borné d'après la mémoire réelle, 2 Gio par tâche. ninja n'a aucune variable de parallélisme, mais meson-python lit « NINJA », le CHEMIN de l'exécutable — vérifié dans mesonpy 0.20. On y met une enveloppe qui ajoute le -j. C'est ce qui sauve matplotlib ; MAKEFLAGS et CMAKE_BUILD_PARALLEL_LEVEL couvrent les autres. Assisted-by: Claude Opus 5 --- script/install/install_debian_dependency.sh | 6 + script/install/install_fedora_dependency.sh | 7 ++ script/install/install_locally.sh | 10 ++ script/install/install_suse_dependency.sh | 7 ++ script/install/lib_lowmem.sh | 123 ++++++++++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 script/install/lib_lowmem.sh diff --git a/script/install/install_debian_dependency.sh b/script/install/install_debian_dependency.sh index d8cb64f..61e759b 100755 --- a/script/install/install_debian_dependency.sh +++ b/script/install/install_debian_dependency.sh @@ -2,6 +2,7 @@ . ./env_var.sh . ./script/install/lib_qpdf.sh +. ./script/install/lib_lowmem.sh EL_USER=${USER} #EL_INSTALL_WKHTMLTOPDF="True" @@ -72,6 +73,11 @@ fi #-------------------------------------------------- if [ "$(uname -m)" = "s390x" ]; then echo "Arch s390x detected" + # La mémoire est la ressource qui manque en premier ici : cc1plus demande + # jusqu'à 2,5 Gio pour un seul fichier de matplotlib, et le tueur du noyau + # abrège sans jamais nommer la mémoire (« Killed signal terminated program + # cc1plus »). On complète par du swap avant d'en arriver là. + el_swap_ensure # Sur une VM fraîche, l'index apt peut être vide : « apt install » échouerait # alors sur TOUT le lot, et l'absence d'un seul paquet ne se voit que bien # plus loin, sous la forme d'une commande introuvable. diff --git a/script/install/install_fedora_dependency.sh b/script/install/install_fedora_dependency.sh index b60d38d..eb571e4 100755 --- a/script/install/install_fedora_dependency.sh +++ b/script/install/install_fedora_dependency.sh @@ -11,6 +11,7 @@ . ./env_var.sh . ./script/install/lib_qpdf.sh +. ./script/install/lib_lowmem.sh EL_USER=${USER} @@ -100,6 +101,12 @@ sudo dnf group install -y ${DNF_SKIP} development c-development \ # « The headers or library files could not be found for jpeg » pour pillow. if [ "$(uname -m)" = "s390x" ]; then echo -e "\n---- Dependances de compilation s390x ----" + # La mémoire est la ressource qui manque en premier ici : cc1plus + # demande jusqu'à 2,5 Gio pour un seul fichier de matplotlib, et le + # tueur du noyau abrège sans jamais nommer la mémoire (« Killed signal + # terminated program cc1plus »). On complète par du swap avant d'en + # arriver là. + el_swap_ensure # Best-effort : un nom qui change d'une version à l'autre ne doit pas # emporter le lot. Ce qui est vraiment indispensable est déjà installé # au-dessus (compilateurs) ou plus bas (pyenv, PostgreSQL). diff --git a/script/install/install_locally.sh b/script/install/install_locally.sh index fe71f79..d73d13d 100755 --- a/script/install/install_locally.sh +++ b/script/install/install_locally.sh @@ -28,6 +28,8 @@ POETRY_ODOO_PATH=${VENV_ODOO_PATH}/bin/poetry # Choix pip/uv : un seul endroit décide, comme pour mise/pyenv. # shellcheck source=script/install/lib_pip_provider.sh . ./script/install/lib_pip_provider.sh +# Bornage des compilations : voir el_build_limit_jobs. +. ./script/install/lib_lowmem.sh export WITH_POETRY_INSTALLATION=1 # Verbosité de l'installation Poetry. « -q » a été retiré du défaut : dans Cleo, @@ -153,6 +155,14 @@ if [[ "${EL_PHASE}" != "setup" ]]; then echo "Poetry introuvable a ${POETRY_ODOO_PATH}, arret." exit 1 fi + # Là où rien n'a de roue, « poetry install » COMPILE des centaines de + # paquets, et le moteur de build de chacun lance nproc tâches en + # parallèle. cc1plus demande jusqu'à 2,5 Gio pour un seul fichier de + # matplotlib : six cœurs épuisent 8 Gio, et le noyau tue le compilateur + # sans jamais nommer la mémoire. On borne d'après la mémoire disponible. + if [[ "$(uname -m)" == "s390x" ]]; then + el_build_limit_jobs + fi "${POETRY_ODOO_PATH}" --version # To fix keyring problem when installation is blocked, use export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring diff --git a/script/install/install_suse_dependency.sh b/script/install/install_suse_dependency.sh index 8170bf5..40eeed8 100755 --- a/script/install/install_suse_dependency.sh +++ b/script/install/install_suse_dependency.sh @@ -15,6 +15,7 @@ . ./env_var.sh . ./script/install/lib_qpdf.sh +. ./script/install/lib_lowmem.sh EL_USER=${USER} @@ -224,6 +225,12 @@ echo -e "\n--- Installing suse dependency --" # compiler ». Tumbleweed livre 1.94, bien au-dessus du 1.78 qu'exige le # Cargo.lock v4 de cryptography. On évite ainsi ~200 Mo inutiles sur amd64. if [ "$(uname -m)" = "s390x" ]; then + # La mémoire est la ressource qui manque en premier ici : cc1plus + # demande jusqu'à 2,5 Gio pour un seul fichier de matplotlib, et le + # tueur du noyau abrège sans jamais nommer la mémoire (« Killed signal + # terminated program cc1plus »). On complète par du swap avant d'en + # arriver là. + el_swap_ensure zyp_soft rust cargo if ! command -v cargo > /dev/null 2>&1; then # FATAL, et non plus un simple avertissement : sur s390x, bcrypt et diff --git a/script/install/lib_lowmem.sh b/script/install/lib_lowmem.sh new file mode 100644 index 0000000..00a3eda --- /dev/null +++ b/script/install/lib_lowmem.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +# +# Bibliothèque SOURÇABLE : compiler sans se faire tuer par manque de mémoire. +# +# Le symptôme est toujours le même, et il ne nomme jamais la mémoire : +# c++: fatal error: Killed signal terminated program cc1plus +# « Killed » est un SIGKILL, donc le tueur de mémoire du noyau. Il est arrivé +# sur matplotlib, dont _backend_agg_wrapper.cpp instancie assez de gabarits +# C++ pour demander 1,5 à 2,5 Gio à cc1plus — À LUI SEUL. +# +# Deux causes se cumulent, et il faut les traiter toutes les deux : +# 1. la VM est petite (le catalogue démarre à 1 ou 2 Gio) ; +# 2. le moteur de build lance nproc compilations EN PARALLÈLE, chacune avec +# son propre cc1plus. Six cœurs suffisent à épuiser 8 Gio. +# +# Réservé aux architectures sans roue PyPI par l'appelant : ailleurs pip pose +# des binaires et rien de tout cela ne s'exécute. + +# RAM + swap visés, en Gio. « 0 » désactive entièrement la création de swap, +# pour qui préfère dimensionner sa machine lui-même. +EL_SWAP_TARGET_GIB="${EL_SWAP_TARGET_GIB:-8}" +EL_SWAP_FILE="${EL_SWAP_FILE:-/swapfile.erplibre}" +# Mio de mémoire à réserver par compilation simultanée. 2048 vient du pic +# mesuré de cc1plus sur matplotlib ; en dessous, le tueur revient. +EL_MEM_PER_JOB_MB="${EL_MEM_PER_JOB_MB:-2048}" + +el_mem_mb() { + awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo 2> /dev/null || echo 0 +} + +el_swap_mb() { + awk '/^SwapTotal:/ {print int($2 / 1024)}' /proc/meminfo 2> /dev/null || echo 0 +} + +# Complète la mémoire par un fichier d'échange, si elle manque. +# +# Volontairement NON persisté dans /etc/fstab : un swap déclaré et disparu +# dégrade le démarrage, et cette machine n'en a besoin que le temps de +# l'installation. La fonction est idempotente, elle repassera au besoin. +el_swap_ensure() { + local ram swap missing free_mb size + [ "${EL_SWAP_TARGET_GIB}" -gt 0 ] 2> /dev/null || return 0 + ram="$(el_mem_mb)" + swap="$(el_swap_mb)" + missing=$((EL_SWAP_TARGET_GIB * 1024 - ram - swap)) + if [ "${missing}" -le 0 ]; then + echo " memoire : ${ram} Mio + ${swap} Mio de swap, suffisant." + return 0 + fi + if [ -e "${EL_SWAP_FILE}" ]; then + # Déjà là mais pas actif : un « swapon » suffit, on ne le réécrit pas. + sudo swapon "${EL_SWAP_FILE}" 2> /dev/null && { + echo " swap : ${EL_SWAP_FILE} reactive." + return 0 + } + fi + # Ne jamais remplir le disque pour gagner de la mémoire : au plus la moitié + # de ce qui reste libre. Un disque plein casse l'installation autrement. + free_mb="$(df -Pm / | awk 'NR==2 {print $4}')" + size="${missing}" + [ "${size}" -gt $((free_mb / 2)) ] && size=$((free_mb / 2)) + if [ "${size}" -lt 512 ]; then + echo " swap : pas assez d'espace disque libre (${free_mb} Mio), abandon." >&2 + return 0 + fi + echo " memoire : ${ram} Mio seulement — ajout de ${size} Mio de swap." + # fallocate est instantané ; dd est le repli des systèmes de fichiers qui + # ne le supportent pas (et un swap sur trous ne fonctionnerait pas). + if ! sudo fallocate -l "${size}M" "${EL_SWAP_FILE}" 2> /dev/null; then + sudo dd if=/dev/zero of="${EL_SWAP_FILE}" bs=1M count="${size}" \ + status=none 2> /dev/null || { + echo " swap : creation impossible, on continue sans." >&2 + return 0 + } + fi + sudo chmod 600 "${EL_SWAP_FILE}" + if sudo mkswap "${EL_SWAP_FILE}" > /dev/null 2>&1 \ + && sudo swapon "${EL_SWAP_FILE}"; then + echo " swap : ${size} Mio actifs (${EL_SWAP_FILE})." + else + echo " swap : activation impossible, on continue sans." >&2 + sudo rm -f "${EL_SWAP_FILE}" + fi +} + +# Nombre de compilations simultanées que la mémoire tolère. +el_build_jobs() { + local total cpus jobs + total=$(($(el_mem_mb) + $(el_swap_mb))) + cpus="$(nproc 2> /dev/null || echo 1)" + jobs=$((total / EL_MEM_PER_JOB_MB)) + [ "${jobs}" -lt 1 ] && jobs=1 + [ "${jobs}" -gt "${cpus}" ] && jobs="${cpus}" + echo "${jobs}" +} + +# Exporte de quoi brider les moteurs de build. À appeler AVANT « poetry +# install », dans le shell qui le lance. +# +# make MAKEFLAGS, lu par tout Makefile. +# cmake CMAKE_BUILD_PARALLEL_LEVEL, lu par « cmake --build » (manifold3d). +# ninja aucune variable de parallélisme n'existe — mais meson-python lit +# « NINJA », le CHEMIN de l'exécutable (vérifié dans mesonpy 0.20 : +# « env_ninja = os.environ.get('NINJA') »). On y met une enveloppe +# qui ajoute le -j. C'est ce qui sauve matplotlib. +# MAX_JOBS convention de PyTorch et de quelques autres. +el_build_limit_jobs() { + local jobs real wrapper + jobs="$(el_build_jobs)" + export MAKEFLAGS="-j${jobs}" + export CMAKE_BUILD_PARALLEL_LEVEL="${jobs}" + export MAX_JOBS="${jobs}" + real="$(command -v ninja || command -v ninja-build || true)" + if [ -n "${real}" ]; then + wrapper="${TMPDIR:-/tmp}/el-ninja-j${jobs}" + printf '#!/bin/sh\nexec %s -j%s "$@"\n' "${real}" "${jobs}" > "${wrapper}" + chmod +x "${wrapper}" + export NINJA="${wrapper}" + fi + echo " compilation bridee a ${jobs} tache(s) simultanee(s)." +} From f32fa6c111dbd27284ae116a3538d1b11d024460 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 03:12:09 -0400 Subject: [PATCH 24/41] [FIX] qemu desktop: snap packages froze the build for 30 minutes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A graphical Ubuntu VM stopped making progress, with no error, on "Unable to contact the store, trying every minute for the next 30 minutes". Nothing in the log says why, and the install just looks stuck. The cause is ours. We disable snapd a few lines earlier, to stop its refreshes during the install. Then gnome-core recommends "firefox", which on Ubuntu is only a transitional package whose postinst runs "snap install" -- against a snapd we just stopped. It is a RECOMMENDS, with alternatives. So we exclude the snap packages and name epiphany-browser, a real .deb. Excluding firefox alone was not enough: apt fell back to chromium-browser, a transitional package too, and Cinnamon froze the same way on thunderbird. Measured on the VM: with this list, GNOME (844 packages) and Cinnamon (1167) pull none, without an apt error. --- FR --- Une VM Ubuntu graphique cessait d'avancer, sans erreur, sur « Unable to contact the store, trying every minute for the next 30 minutes ». Rien dans le log n'en dit la raison, et l'installation paraît simplement figée. La cause est nôtre. On coupe snapd quelques lignes plus haut, pour empêcher ses rafraîchissements pendant l'installation. Puis gnome-core recommande « firefox », qui n'est plus sur Ubuntu qu'un paquet de transition dont le postinst lance « snap install » — contre un snapd qu'on vient d'arrêter. C'est un RECOMMENDS, avec alternatives. On écarte donc les paquets snap et on nomme epiphany-browser, un vrai .deb. Écarter le seul firefox ne suffisait pas : apt retombait sur chromium-browser, paquet de transition lui aussi, et Cinnamon figeait pareillement sur thunderbird. Mesuré sur la VM : avec cette liste, GNOME (844 paquets) et Cinnamon (1167) n'en tirent aucun, sans erreur apt. Assisted-by: Claude Opus 5 --- script/todo/todo.py | 75 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/script/todo/todo.py b/script/todo/todo.py index 5490ca9..9741d7a 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4352,6 +4352,78 @@ class TODO: "service": "lightdm", }, } + # Ubuntu remplace trois applications par des paquets de TRANSITION dont le + # postinst lance « snap install ». Or snapd est coupé juste avant, pour + # empêcher ses rafraîchissements pendant l'installation : le postinst ne + # joint alors pas le store et RÉESSAIE UNE MINUTE DURANT TRENTE MINUTES. + # L'installation paraît figée et rien dans le log ne dit pourquoi. + # + # La famille est CLOSE et relevée dans l'index du dépôt, pas devinée : trois + # paquets sources portent une version « …snap1… », firefox, chromium-browser + # et thunderbird — avec toutes leurs déclinaisons (firefox-locale-*, + # chromium-codecs-*). Les corriger un à un a coûté deux VM figées : firefox + # sous GNOME, puis thunderbird sous Cinnamon. + # + # Les trois ne sont que RECOMMANDÉS, et avec des solutions de rechange : + # Recommends: firefox-esr | firefox | chromium | epiphany-browser | … + # Recommends: thunderbird | evolution | geary | mail-reader + # On les écarte donc, et on nomme deux vrais .deb pour satisfaire les + # recommandations. Les nommer rend le résultat déterministe : laissé à apt, + # le premier repli était « chromium-browser », un paquet de transition lui + # aussi. + # + # Un épinglage apt sur « Pin: version *snap1* » aurait été plus général — + # essayé en glob et en regex, il ne bloque rien. Mesuré sur une VM 26.04 : + # avec cette liste, GNOME (844 paquets) et Cinnamon (1167) n'en tirent + # AUCUN, sans erreur apt. + _QEMU_APT_NO_SNAP = ( + "epiphany-browser evolution" + " firefox- chromium- chromium-browser- thunderbird-" + ) + + # Magasin d'applications d'une VM graphique. Ubuntu livre snapd dans son + # image cloud (vérifié : 2.75.2 en 26.04) et gnome-core RECOMMANDE + # « firefox », qui n'y est plus qu'un paquet de transition lançant + # « snap install ». Trois réponses possibles, et il faut choisir : + # + # deb rien que des .deb. snapd coupé, paquets-snap écartés, + # epiphany-browser comme navigateur. Le plus léger, et rien à + # télécharger en plus pendant un déploiement déjà long. + # flatpak l'outillage Flatpak en plus, SANS dépôt Flathub ni + # installation : la machine est prête, l'administrateur ajoute + # les dépôts qu'il veut. + # snap le comportement d'Ubuntu, snapd laissé actif et Firefox en + # snap. Lent sous émulation, mais c'est le défaut de la distro. + # + # La question n'a de sens que pour une VM Ubuntu GRAPHIQUE : sur un + # serveur, rien ne tire de snap. + QEMU_APP_STORES = ( + ("deb", "deb only (epiphany-browser)"), + ("flatpak", "Flatpak tooling, no Flathub"), + ("snap", "snap (Ubuntu default, Firefox)"), + ) + QEMU_SNAP_DISTROS = ("ubuntu",) + + @classmethod + def _qemu_desktop_suffixes(cls): + """{clé de saveur: suffixe de nom}. La TUI le reçoit par son contexte + plutôt que de le redéfinir : un seul endroit décrit les saveurs.""" + return {k: v["suffix"] for k, v in cls._QEMU_DESKTOP.items()} + + @classmethod + def _qemu_apt_store_pkgs(cls, app_store): + """Paquets apt à ajouter au bureau selon le magasin retenu.""" + if app_store == "snap": + # On ne retire rien : Firefox arrivera en snap, comme sur une + # Ubuntu ordinaire, et snapd est resté actif pour le servir. + return "" + pkgs = cls._QEMU_APT_NO_SNAP + if app_store == "flatpak": + # Le greffon donne à GNOME Logiciels la gestion des Flatpak. + # Aucun « remote-add » ici : le dépôt reste un choix explicite. + pkgs += " flatpak gnome-software-plugin-flatpak" + return pkgs + # Accès distant, indépendant du bureau choisi. _QEMU_DESKTOP_REMOTE = { "apt": {"packages": "xrdp", "port": 3389, "client": "RDP"}, @@ -4546,7 +4618,8 @@ class TODO: "n=$((n+1)); [ $n -ge 30 ] && break; sleep 10; done; " "sudo DEBIAN_FRONTEND=noninteractive " "apt-get -o DPkg::Lock::Timeout=600 install -y " - f"{de['apt']} {rem['apt']['packages']}; " + f"{de['apt']} {rem['apt']['packages']} " + f"{self._QEMU_APT_NO_SNAP}; " "elif command -v dnf >/dev/null 2>&1; then " # Cascade d'environnements : le premier qui existe gagne. Un # environnement absent fait rendre 1 à dnf sans rien installer, From 1726cb5dabe8fd454c8b82b93702033670355413 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 03:24:19 -0400 Subject: [PATCH 25/41] [ADD] qemu: choose the application store of a graphical VM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit snap was not a choice but a fate: we disabled snapd, then gnome-core pulled a Firefox snap that froze the install for thirty minutes. The previous fix imposed "deb". Three answers are now offered. deb nothing but .deb, epiphany-browser as the browser. Default, and lightest: nothing extra to download. flatpak Flatpak tooling as well, WITHOUT the Flathub remote or any install. The machine is ready, the admin picks its remotes. snap Ubuntu's default, snapd left running and Firefox as a snap. It is the only mode where snapd is not disabled -- disabling it was precisely the cause of the freeze. The question is only asked when it means something: at least one graphical VM on a distro shipping snapd, Ubuntu alone here. A server pulls no snap, and Debian ships none. The TUI greys the choice out and says why; the CLI does not ask. Verified on a 26.04 VM: snapd is indeed preinstalled (2.75.2), flatpak and its GNOME Software plugin are packaged, and firefox-esr does not exist on Ubuntu. --- FR --- snap n'était plus un choix mais une fatalité : on coupait snapd, puis gnome-core tirait un Firefox-snap qui figeait l'installation trente minutes. Le correctif précédent imposait « deb ». Trois réponses sont maintenant offertes. deb rien que des .deb, epiphany-browser comme navigateur. Défaut, et le plus léger : rien de plus à télécharger. flatpak l'outillage Flatpak en plus, SANS dépôt Flathub ni installation. La machine est prête, l'administrateur choisit ses dépôts. snap le défaut d'Ubuntu, snapd laissé actif et Firefox en snap. C'est le seul mode où snapd n'est pas coupé — l'y couper était précisément la cause du blocage. La question n'est posée que lorsqu'elle a un sens : au moins une VM graphique sur une distribution qui livre snapd, Ubuntu seule ici. Un serveur ne tire aucun snap, et Debian n'en livre pas. La TUI grise le choix et dit pourquoi ; la CLI ne le demande pas. Vérifié sur une VM 26.04 : snapd y est bien préinstallé (2.75.2), flatpak et son greffon GNOME Software sont empaquetés, et firefox-esr n'existe pas sur Ubuntu. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 45 ++++++++++++++++++ script/todo/todo.py | 82 +++++++++++++++++++++++++++------ 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 81a6f10..101aca3 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -212,6 +212,7 @@ def build_spec(vms, domains, form): "timezone": form.get("timezone", ""), "desktop": form.get("desktop", ""), "python_provider": form.get("python_provider", ""), + "app_store": form.get("app_store", "deb"), "install": form["install"], "add_ssh_config": form["add_ssh_config"], "parallelism": form["parallelism"], @@ -278,6 +279,10 @@ def run_deploy_form(ctx, run_app: bool = True): desktops = list(ctx.get("desktops") or []) # Architectures pour lesquelles mise publie un binaire. mise_arches = set(ctx.get("mise_arches") or ()) + # [(clé, libellé)] des magasins d'applications, et les distributions qui + # livrent snapd — la question n'a de sens que pour celles-là, graphiques. + app_stores = list(ctx.get("app_stores") or []) + snap_distros = set(ctx.get("snap_distros") or ()) defaults = ctx.get("defaults") or {} result = {"spec": None} @@ -500,6 +505,14 @@ def run_deploy_form(ctx, run_app: bool = True): f"{t('Graphical (server + desktop):')} {label}", value=defaults.get("desktop", "") == key, ) + if app_stores: + yield Static( + t("Application store:"), classes="grouptitle" + ) + with RadioSet(id="f_store"): + for i, (_k, label) in enumerate(app_stores): + yield RadioButton(label, value=i == 0) + yield Static("", id="storewarn") yield Static("ERPLibre", classes="grouptitle") yield Checkbox( t("Install ERPLibre"), @@ -655,6 +668,7 @@ def run_deploy_form(ctx, run_app: bool = True): row["custom"] = bool(self.overrides.get(entry_key(entry))) self._render_plan() self._render_mise() + self._render_store() def _render_mise(self): """Grise le choix quand aucune VM retenue n'est servie par mise, @@ -690,6 +704,36 @@ def run_deploy_form(ctx, run_app: bool = True): index = self.query_one("#f_python", RadioSet).pressed_index return "pyenv" if index == 1 else "mise" + def _app_store(self): + """Magasin retenu. Sans VM concernée, la réponse est « deb » : + elle ne change rien, et laisser passer « snap » réactiverait snapd + pour rien.""" + if not app_stores or not self._app_store_needed(): + return "deb" + index = self.query_one("#f_store", RadioSet).pressed_index + if index is None or not (0 <= index < len(app_stores)): + return app_stores[0][0] + return app_stores[index][0] + + def _app_store_needed(self): + """Au moins une VM graphique sur une distribution qui livre snapd.""" + return any( + vm.get("desktop") and vm["distro"] in snap_distros + for vm in self.vms + ) + + def _render_store(self): + """Grise le choix quand aucune VM ne le concerne, et dit pourquoi.""" + if not app_stores: + return + needed = self._app_store_needed() + self.query_one("#f_store", RadioSet).disabled = not needed + self.query_one("#storewarn", Static).update( + "" + if needed + else f" {t('No graphical VM on a snap-based distro.')}" + ) + def _mise_usable(self): return any(vm["arch"] in mise_arches for vm in self.vms) @@ -1157,6 +1201,7 @@ def run_deploy_form(ctx, run_app: bool = True): or "", "desktop": self._default_desktop(), "python_provider": self._python_provider(), + "app_store": self._app_store(), "install": install, "add_ssh_config": self.query_one("#f_sshcfg", Checkbox).value, # Une exécution par installation : le nombre de VM retenues diff --git a/script/todo/todo.py b/script/todo/todo.py index 9741d7a..ad78f7e 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4457,7 +4457,7 @@ class TODO: ) @staticmethod - def _qemu_no_auto_upgrade(prod): + def _qemu_no_auto_upgrade(prod, app_store="deb"): """Coupe les mises à jour automatiques sur une VM de DÉVELOPPEMENT. Vécu sur erplibre-ubuntu-2404 : unattended-upgrades s'est déclenché en @@ -4485,13 +4485,20 @@ class TODO: "fi; " # snapd : 57 s sur le CHEMIN CRITIQUE du démarrage, mesurés par # « systemd-analyze critical-chain » sur une VM s390x — - # multi-user.target attend snapd.seeded. Aucune VM ERPLibre - # n'installe de snap : c'est du temps payé pour rien, à chaque - # démarrage. On désactive plutôt que désinstaller, pour rester - # réversible d'un « systemctl enable ». - "sudo systemctl disable --now snapd.seeded.service " - "snapd.service snapd.socket snapd.apparmor.service " - ">/dev/null 2>&1 || true; " + # multi-user.target attend snapd.seeded. C'est du temps payé pour + # rien quand aucun snap n'est voulu. On désactive plutôt que + # désinstaller, pour rester réversible d'un « systemctl enable ». + # + # Sauf si le magasin RETENU est snap : le couper puis laisser un + # postinst appeler « snap install » est exactement ce qui figeait + # une VM graphique trente minutes durant. + + ( + "" + if app_store == "snap" + else "sudo systemctl disable --now snapd.seeded.service " + "snapd.service snapd.socket snapd.apparmor.service " + ">/dev/null 2>&1 || true; " + ) ) # Miroirs openSUSE préférés, du plus proche au dernier recours. Le @@ -4602,7 +4609,7 @@ class TODO: f'echo " {t("openSUSE mirror:")} $zm"; break; fi; done; ' ) - def _qemu_desktop_remote_cmd(self, flavour="gnome"): + def _qemu_desktop_remote_cmd(self, flavour="gnome", app_store="deb"): """Bloc shell installant le bureau choisi + son accès distant, quelle que soit la distribution. Même aiguillage que l'installation ERPLibre, et même traitement du verrou apt : cette étape passe par la commande @@ -4619,7 +4626,7 @@ class TODO: "sudo DEBIAN_FRONTEND=noninteractive " "apt-get -o DPkg::Lock::Timeout=600 install -y " f"{de['apt']} {rem['apt']['packages']} " - f"{self._QEMU_APT_NO_SNAP}; " + f"{self._qemu_apt_store_pkgs(app_store)}; " "elif command -v dnf >/dev/null 2>&1; then " # Cascade d'environnements : le premier qui existe gagne. Un # environnement absent fait rendre 1 à dnf sans rien installer, @@ -4704,6 +4711,7 @@ class TODO: prod=False, desktop=False, python_provider="", + app_store="deb", ): """Script exécuté DANS la VM. `branch` à None n'installe QUE le bureau — le choix graphique ne dépend pas d'ERPLibre, et une VM peut être @@ -4723,8 +4731,8 @@ class TODO: return ( "set -e; " + self._qemu_cloud_init_wait() - + self._qemu_no_auto_upgrade(prod) - + self._qemu_desktop_remote_cmd(desktop) + + self._qemu_no_auto_upgrade(prod, app_store) + + self._qemu_desktop_remote_cmd(desktop, app_store) ) if not final_cmd: final_cmd = f"make install_os && make {self.ERPLIBRE_ODOO_TARGET}" @@ -4749,7 +4757,7 @@ class TODO: # secondaire bienvenu : les timers apt-daily ne tiennent plus le verrou # apt pendant l'installation. En PROD on ne touche à rien : les # correctifs de sécurité automatiques doivent rester actifs. - no_auto_upgrade = self._qemu_no_auto_upgrade(prod) + no_auto_upgrade = self._qemu_no_auto_upgrade(prod, app_store) return ( "set -e; " + self._qemu_cloud_init_wait() # Coupé AVANT les apt-get ci-dessous : sinon apt-daily peut reprendre @@ -4758,7 +4766,12 @@ class TODO: # Le bureau d'abord : il repose sur les dépôts de la distribution, # là où l'installation ERPLibre compile longuement. Un échec ici se # voit donc tôt plutôt qu'après une heure. - + (self._qemu_desktop_remote_cmd(desktop) if desktop else "") + + + ( + self._qemu_desktop_remote_cmd(desktop, app_store) + if desktop + else "" + ) + + # Outils d'amorçage (absents des images cloud minimales) : curl, # git, make. Chaque branche RAFRAÎCHIT d'abord les dépôts pour que # la VM soit la plus rapide possible (miroirs à jour / les plus @@ -4859,6 +4872,7 @@ class TODO: prod=False, desktop=False, python_provider="", + app_store="deb", ): """Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre le dashboard Textual. Quitter le dashboard n'arrête pas les installs. @@ -4881,6 +4895,7 @@ class TODO: prod, "" if desk_map else desktop, python_provider, + app_store, ) try: mod = self._qemu_import_module() @@ -4911,6 +4926,7 @@ class TODO: prod, desk_map.get(name, ""), python_provider, + app_store, ) vms.append(entry) else: @@ -4953,6 +4969,7 @@ class TODO: prod=False, desktop=False, python_provider="", + app_store="deb", ): """Clone ERPLibre (branche donnée) dans la VM puis exécute la commande d'install du profil choisi (streamé). `ip` : IP déjà résolue ; @@ -4974,7 +4991,7 @@ class TODO: ) return remote = self._qemu_erplibre_remote_cmd( - branch, final_cmd, prod, desktop, python_provider + branch, final_cmd, prod, desktop, python_provider, app_store ) ssh_opts = ( "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " @@ -5689,6 +5706,8 @@ class TODO: "extra_disk_gb": self.ERPLIBRE_EXTRA_DISK_GB, "desktop_disk_gb": self.QEMU_DESKTOP_EXTRA_DISK_GB, "mise_arches": self.QEMU_MISE_ARCHES, + "app_stores": [(k, t(lbl)) for k, lbl in self.QEMU_APP_STORES], + "snap_distros": self.QEMU_SNAP_DISTROS, "desktops": [ (k, v["label"]) for k, v in self._QEMU_DESKTOP.items() ], @@ -5979,6 +5998,30 @@ class TODO: return "" return flavours[index] if 0 <= index < len(flavours) else "" + @classmethod + def _qemu_app_store_needed(cls, vms): + """Vrai si au moins une VM du parc est à la fois graphique et d'une + distribution qui livre snapd. Ailleurs la question n'a pas d'objet : + un serveur ne tire aucun snap, et Debian ou Fedora n'en livrent pas.""" + return any( + vm.get("desktop") and vm.get("distro") in cls.QEMU_SNAP_DISTROS + for vm in vms + ) + + def _qemu_ask_app_store(self, vms): + """Magasin d'applications des VM graphiques Ubuntu.""" + if not self._qemu_app_store_needed(vms): + return "deb" + print(f"\n{t('Application store (graphical Ubuntu VMs):')}") + for i, (_key, label) in enumerate(self.QEMU_APP_STORES, 1): + star = " *" if i == 1 else "" + print(f" [{i}] {t(label)}{star}") + print(f" ⚠ {t('snap needs the store; slow under emulation.')}") + answer = input(f"{t('Choice')} [1]: ").strip() or "1" + if answer.isdigit() and 1 <= int(answer) <= len(self.QEMU_APP_STORES): + return self.QEMU_APP_STORES[int(answer) - 1][0] + return "deb" + def _qemu_ask_python_provider(self, arches): """mise (CPython précompilé) ou pyenv (compilation). @@ -6064,6 +6107,11 @@ class TODO: timezone = self._qemu_ask_timezone() locale = self._qemu_ask_locale() desktop = self._qemu_ask_desktop() + # La CLI ne pose qu'un type pour tout le parc : on le recopie sur chaque + # VM avant de décider du magasin, qui ne concerne que les graphiques. + for _vm in vms: + _vm.setdefault("desktop", desktop) + app_store = self._qemu_ask_app_store(vms) python_provider = self._qemu_ask_python_provider( [vm["arch"] for vm in vms] ) @@ -6145,6 +6193,7 @@ class TODO: "locale": locale, "desktop": desktop, "python_provider": python_provider, + "app_store": app_store, "install": install, "add_ssh_config": add_ssh_config, "parallelism": parallelism, @@ -6225,6 +6274,7 @@ class TODO: desktop_map.setdefault(_name, desktop_default) desktop = next((d for d in desktop_map.values() if d), "") python_provider = spec.get("python_provider") or "" + app_store = spec.get("app_store") or "deb" ssh_key = spec.get("ssh_key") add_ssh_config = spec["add_ssh_config"] parallelism = spec["parallelism"] @@ -6300,6 +6350,7 @@ class TODO: install["prod"] if install else False, desktop=desktop_map, python_provider=python_provider, + app_store=app_store, ) else: print( @@ -6316,6 +6367,7 @@ class TODO: install["prod"], desktop=desktop_map.get(name, ""), python_provider=python_provider, + app_store=app_store, ) # Sommaire TOTAL (déploiement + résolution IP + ssh_config + install From 3fda367151574c90af32b4ec79b4fc7177b193ef Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 03:29:33 -0400 Subject: [PATCH 26/41] [ADD] qemu: a graphical VM name carries its desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit erplibre-ubuntu-2404-gnome, erplibre-ubuntu-2404-mint. A graphical VM is recognisable from a "virsh list", and its hostname says so too -- deploy_qemu uses the name as hostname when --hostname is not given. This is not only cosmetic. The name is the COLLISION KEY: a graphical VM and its server twin carried the same one, so the second was reported as "already exists" and silently skipped. They are now distinct. The suffix is applied AFTER overrides, the only point where each machine's type is known now that it is chosen VM by VM. In the form the name therefore follows the choice live, both ways. "mint" rather than "cinnamon": that is the name chosen for the fleet, the installed package still being Cinnamon from the distribution's own repositories. The suffix lives with the flavour, in _QEMU_DESKTOP, and the CLI calls the same function as the TUI rather than writing a second. --- FR --- erplibre-ubuntu-2404-gnome, erplibre-ubuntu-2404-mint. Une VM graphique se reconnaît d'un « virsh list », et son nom d'hôte le dit aussi — deploy_qemu prend le nom pour hôte quand --hostname n'est pas donné. Ce n'est pas que cosmétique. Le nom sert de CLÉ DE COLLISION : une VM graphique et sa jumelle serveur portaient le même, donc la seconde était signalée « existe déjà » et silencieusement ignorée. Elles se distinguent maintenant. Le suffixe est appliqué APRÈS les surcharges, seul moment où le type de chaque machine est connu depuis qu'il se choisit VM par VM. Dans le formulaire, le nom suit donc le choix en direct, dans les deux sens. « mint » et non « cinnamon » : c'est le nom retenu pour le parc, le paquet installé restant Cinnamon depuis les dépôts de la distribution. Le suffixe vit avec la saveur, dans _QEMU_DESKTOP, et la CLI appelle la même fonction que la TUI plutôt que d'en écrire une seconde. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 33 +++++++++++++++++++++++++++++++-- script/todo/todo.py | 16 ++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 101aca3..b7ad1fe 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -149,15 +149,40 @@ def apply_overrides(vms, entries, overrides): return vms +def vm_name(base, desktop, suffixes): + """Nom de VM, suffixé du bureau quand il y en a un. + + Le nom sert de nom d'hôte ET de clé de collision : une VM graphique et sa + jumelle serveur doivent donc porter des noms différents, sinon la seconde + est signalée « existe déjà » et silencieusement ignorée. Idempotent, le + nom étant recalculé à chaque frappe.""" + suffix = (suffixes or {}).get(desktop or "") + if not suffix or base.endswith(f"-{suffix}"): + return base + return f"{base}-{suffix}" + + def build_vms( - entries, profile, base_vcpus, host_cpu, custom, overrides, desktop="" + entries, + profile, + base_vcpus, + host_cpu, + custom, + overrides, + desktop="", + suffixes=None, ): """Catalogue choisi + profil + surcharges -> liste de VM de la spec.""" - return apply_overrides( + vms = apply_overrides( apply_profile(entries, profile, base_vcpus, host_cpu, custom, desktop), entries, overrides, ) + # APRÈS les surcharges : c'est là seulement que le type de chaque VM est + # connu, puisqu'il se choisit machine par machine. + for vm in vms: + vm["name"] = vm_name(vm["name"], vm.get("desktop"), suffixes) + return vms def vm_status(name, domains): @@ -277,6 +302,9 @@ def run_deploy_form(ctx, run_app: bool = True): desktop_disk = ctx.get("desktop_disk_gb") or 0 # [(clé, libellé)] — la liste vient de todo.py, source unique. desktops = list(ctx.get("desktops") or []) + # {clé de saveur: suffixe de nom}, fourni par todo.py qui décrit les + # saveurs — on ne le redéfinit pas ici. + desktop_suffixes = dict(ctx.get("desktop_suffixes") or {}) # Architectures pour lesquelles mise publie un binaire. mise_arches = set(ctx.get("mise_arches") or ()) # [(clé, libellé)] des magasins d'applications, et les distributions qui @@ -647,6 +675,7 @@ def run_deploy_form(ctx, run_app: bool = True): self.custom, self.overrides, self._default_desktop(), + desktop_suffixes, ) # ERPLibre et GNOME pèsent chacun sur le disque, et se cumulent. grow = 0 diff --git a/script/todo/todo.py b/script/todo/todo.py index ad78f7e..db3253e 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4338,6 +4338,11 @@ class TODO: "pacman": "gnome gdm", "zypper": "patterns-gnome-gnome_basic gdm", "service": "gdm", + # Suffixe ajouté au nom de VM, donc au nom d'hôte. Une VM + # graphique se reconnaît alors d'un « virsh list », et deux VM de + # même distribution mais de types différents ne se marchent plus + # dessus — le nom sert aussi de clé de collision. + "suffix": "gnome", }, "cinnamon": { "label": "Cinnamon (Linux Mint)", @@ -4350,6 +4355,10 @@ class TODO: "pacman": "cinnamon lightdm lightdm-gtk-greeter", "zypper": "cinnamon lightdm", "service": "lightdm", + # « mint » plutôt que « cinnamon » : c'est le nom retenu pour le + # parc. Le paquet installé reste bien Cinnamon, depuis les dépôts + # de la distribution et non ceux de Mint. + "suffix": "mint", }, } # Ubuntu remplace trois applications par des paquets de TRANSITION dont le @@ -5708,6 +5717,7 @@ class TODO: "mise_arches": self.QEMU_MISE_ARCHES, "app_stores": [(k, t(lbl)) for k, lbl in self.QEMU_APP_STORES], "snap_distros": self.QEMU_SNAP_DISTROS, + "desktop_suffixes": self._qemu_desktop_suffixes(), "desktops": [ (k, v["label"]) for k, v in self._QEMU_DESKTOP.items() ], @@ -6109,8 +6119,14 @@ class TODO: desktop = self._qemu_ask_desktop() # La CLI ne pose qu'un type pour tout le parc : on le recopie sur chaque # VM avant de décider du magasin, qui ne concerne que les graphiques. + # Le nom suit le type, exactement comme dans le formulaire — c'est la + # même fonction, pas une seconde implémentation. + from script.todo.qemu_deploy_form import vm_name + + suffixes = self._qemu_desktop_suffixes() for _vm in vms: _vm.setdefault("desktop", desktop) + _vm["name"] = vm_name(_vm["name"], _vm.get("desktop"), suffixes) app_store = self._qemu_ask_app_store(vms) python_provider = self._qemu_ask_python_provider( [vm["arch"] for vm in vms] From 18f5ddd69ffc7a78229ef7b810dae3726f55b6fe Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 03:53:04 -0400 Subject: [PATCH 27/41] [ADD] qemu monitor: act on a VM without leaving the dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The monitor could only watch. The "a" key now opens the selected VM's actions: update, restart Odoo, delete. The update is chosen by parts, and the order is not free: system packages, then git repositories, then Python dependencies -- the latter compile against the former. Nothing is chained with "&&": one failing part must not take the others down. Deleting demands a second hand, a confirmation screen naming the disk to be erased. Two defects the dashboard revealed. Detached installs were stealing the shell's keystrokes, the child inheriting a terminal it had no business reading. And the elapsed time restarted from zero every time the dashboard was reopened, since it was counted from the view rather than from the run's own first write. --- FR --- Le suivi ne savait que regarder. La touche « a » ouvre désormais les actions de la VM sélectionnée : mettre à jour, redémarrer Odoo, supprimer. La mise à jour se choisit par parties, et l'ordre n'est pas libre : paquets système, puis dépôts git, puis dépendances Python — ces dernières compilent contre les premiers. Rien n'est enchaîné par « && » : une partie en échec ne doit pas emporter les autres. Supprimer exige une seconde main, un écran de confirmation nommant le disque à effacer. Deux défauts que le tableau de bord a révélés. Les installations détachées volaient les frappes du shell, l'enfant héritant d'un terminal qu'il n'avait pas à lire. Et la durée repartait de zéro à chaque réouverture, comptée depuis la vue plutôt que depuis la première écriture du run. Assisted-by: Claude Opus 5 --- script/todo/qemu_install_monitor.py | 402 +++++++++++++++++++++++++++- 1 file changed, 396 insertions(+), 6 deletions(-) diff --git a/script/todo/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index dc6664f..9d33b2a 100644 --- a/script/todo/qemu_install_monitor.py +++ b/script/todo/qemu_install_monitor.py @@ -37,6 +37,11 @@ SSH_OPTS = ( "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " "-o ConnectTimeout=8" ) +# Pour les ssh NON INTERACTIFS : « -n » branche leur entrée sur /dev/null. +# Ceinture et bretelles avec le stdin du processus détaché — un ssh qui lit le +# terminal vole les frappes du shell, et le diagnostic est très difficile. +# Jamais pour un ssh interactif (touche « s »), qui doit garder le clavier. +SSH_OPTS_BATCH = f"{SSH_OPTS} -n" def session_dir() -> Path: @@ -184,7 +189,7 @@ def _launch_one( f"seen=0; " f"for i in $(seq 1 240); do " f"{refresh}" - f'st=$(ssh {SSH_OPTS} -o BatchMode=yes "erplibre@$ip" ' + f'st=$(ssh {SSH_OPTS_BATCH} -o BatchMode=yes "erplibre@$ip" ' f"{shlex.quote(ci_probe)} 2>/dev/null); " f'case "$st" in ' f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) seen=1; break;; " @@ -200,13 +205,22 @@ def _launch_one( f"echo {shlex.quote('== ' + msg_ready + ' ==')} >> {log_q}; " f"else echo {shlex.quote('== ' + msg_giveup + ' ==')} >> {log_q}; fi; " f'echo " → $ip" >> {log_q}; ' - f'ssh {SSH_OPTS} "erplibre@$ip" {shlex.quote(remote_cmd)} ' + f'ssh {SSH_OPTS_BATCH} "erplibre@$ip" {shlex.quote(remote_cmd)} ' f">> {log_q} 2>&1; " f'echo "{EXIT_MARKER} $?" >> {log_q}' ) # setsid -f : le process survit à la fermeture du menu / du dashboard. + # stdin sur /dev/null : SANS lui, le descripteur 0 du processus détaché + # reste le TERMINAL. « setsid » lui retire le terminal de contrôle, mais ne + # ferme aucun descripteur — et ssh, lui, LIT son entrée pour la transmettre + # à la commande distante. Deux lecteurs se partagent alors le clavier : une + # frappe sur deux part vers l'installation au lieu du shell, et il faut + # appuyer plusieurs fois pour qu'une lettre arrive. Le symptôme survit à + # todo.py, puisque l'installation continue une demi-heure après sa + # fermeture — d'où un terminal qui « bogue » sans cause visible. subprocess.Popen( ["setsid", "-f", "bash", "-c", wrapper], + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, @@ -277,6 +291,20 @@ def launch_installs(vms: list[dict], branch: str, remote_cmd: str) -> str: return manifest_path +def finished_at(log_path: str, fallback: float) -> float: + """Instant où l'installation s'est RÉELLEMENT arrêtée. + + La dernière écriture dans le log, c'est-à-dire le marqueur de sortie. On + ne peut pas prendre « maintenant » : le suivi est détachable, et il + observe souvent l'état final longtemps après. Rouvrir le tableau de bord + une heure plus tard ajoutait cette heure à la durée affichée, comme si + l'installation avait tourné pendant tout ce temps.""" + try: + return os.path.getmtime(log_path) + except OSError: + return fallback + + def read_status(log_path: str) -> tuple[str, int | None]: """(état, code) d'un log : pending / running / done / failed. Ne lit que la FIN du fichier (le marqueur de sortie est sur la dernière ligne) : lire @@ -814,13 +842,118 @@ def virsh_domstates() -> dict: # --------------------------------------------------------------------------- # # Dashboard Textual # --------------------------------------------------------------------------- # +# Parties d'une remise à jour, dans l'ordre où elles doivent tourner : les +# paquets système d'abord (compilateurs et en-têtes), le code ensuite, les +# dépendances Python en dernier — elles se compilent contre les deux premiers. +UPDATE_PARTS = ( + ("system", "Paquets systeme (apt/dnf/pacman/zypper)"), + ("git", "Depots git (ERPLibre + addons)"), + ("python", "Dependances Python (poetry)"), +) +EL_DIR = "~/git/erplibre" + + +def update_remote_cmd(parts) -> str: + """Commande exécutée DANS la VM pour remettre à jour ce qui est coché. + + Rien n'est enchaîné par « && » : une partie qui échoue ne doit pas + empêcher les suivantes, et son code de retour se lit dans la sortie. + Chaque bloc s'annonce, sinon un log de mise à jour est illisible.""" + chosen = [p for p in parts if p in dict(UPDATE_PARTS)] + if not chosen: + return "" + out = ["set -u"] + if "system" in chosen: + out.append( + 'echo "== Paquets systeme =="; ' + "if command -v apt-get >/dev/null 2>&1; then " + "sudo apt-get -o DPkg::Lock::Timeout=600 update -qq " + "&& sudo DEBIAN_FRONTEND=noninteractive " + "apt-get -o DPkg::Lock::Timeout=600 -y upgrade; " + "elif command -v dnf >/dev/null 2>&1; then " + "sudo dnf -y upgrade --refresh; " + "elif command -v pacman >/dev/null 2>&1; then " + # Arch ne supporte pas la mise à jour partielle : -Syu, jamais -S. + "pgrep -x pacman >/dev/null 2>&1 " + "|| sudo rm -f /var/lib/pacman/db.lck; " + "sudo pacman -Syu --noconfirm; " + "elif command -v zypper >/dev/null 2>&1; then " + ". /etc/os-release; " + 'case "$ID" in *tumbleweed*) ' + "sudo zypper --non-interactive dup " + "--auto-agree-with-licenses --allow-vendor-change;; " + "*) sudo zypper --non-interactive up " + "--auto-agree-with-licenses;; esac; " + 'else echo "Gestionnaire de paquets inconnu"; fi; ' + ) + if "git" in chosen: + out.append( + 'echo "== Depots git =="; ' + f"cd {EL_DIR} || exit 1; " + # --ff-only : une VM ne doit jamais fusionner toute seule. Un + # historique divergent s'arrête ici, visiblement. + "git pull --ff-only; " + # Les addons viennent de Google Repo, pas de git : c'est le script + # de l'installation qui sait les synchroniser. + "./script/install/install_git_repo.sh; " + ) + if "python" in chosen: + out.append( + 'echo "== Dependances Python =="; ' + f"cd {EL_DIR} || exit 1; " + # La phase « poetry » seule : ni venvs ni repo, juste les paquets. + "EL_PHASE=poetry ./script/install/install_locally.sh; " + ) + out.append('echo "== Mise a jour terminee =="') + return "".join(out[:1]) + "; " + "".join(out[1:]) + + +def restart_odoo_cmd() -> str: + """Redémarre le service ERPLibre, ou le lance à la main s'il n'existe pas. + + Une VM installée sans profil de production n'a pas forcément l'unité + systemd : le repli dit quoi faire plutôt que d'échouer sans un mot.""" + return ( + "if systemctl list-unit-files 2>/dev/null | grep -q '^erplibre'; then " + "sudo systemctl restart erplibre.service " + "&& sudo systemctl --no-pager --lines=15 status erplibre.service; " + "else echo 'Pas de service erplibre : lancez ./run.sh dans " + f"{EL_DIR}'; fi" + ) + + +def delete_vm_cmd(name: str, with_disks: bool) -> str: + """Efface la VM sur l'HÔTE. Même séquence que « TODO._qemu_delete_vm » : + arrêt, retrait de la définition (nvram si UEFI, repli sinon), puis les + disques à la demande.""" + q = shlex.quote(name) + cmd = ( + f"sudo virsh destroy {q} 2>/dev/null; " + f"sudo virsh undefine {q} --nvram 2>/dev/null " + f"|| sudo virsh undefine {q}" + ) + if with_disks: + disk = shlex.quote(f"/var/lib/libvirt/images/{name}.qcow2") + seed = shlex.quote(f"/var/lib/libvirt/images/iso/{name}-seed.iso") + cmd += f"; sudo rm -f {disk} {seed}" + return cmd + + def run_monitor(manifest_path: str, run_app: bool = True): """Ouvre le dashboard Textual sur un manifeste d'installation. `run_app` à False renvoie l'instance sans la lancer (tests headless).""" from textual.app import App, ComposeResult from textual.containers import Horizontal, Vertical from textual.screen import ModalScreen - from textual.widgets import DataTable, Footer, Header, RichLog, Static + from textual.widgets import ( + Button, + Checkbox, + DataTable, + Footer, + Header, + RichLog, + Static, + ) manifest = json.loads(Path(manifest_path).read_text()) started = manifest.get("started", time.time()) @@ -869,6 +1002,84 @@ def run_monitor(manifest_path: str, run_app: bool = True): def action_dismiss(self) -> None: self.dismiss() + class ConfirmScreen(ModalScreen): + """Confirmation d'une action irréversible. Le bouton dangereux n'est + PAS le défaut : il faut le viser, pas juste appuyer sur Entrée.""" + + BINDINGS = [("escape", "cancel", "Annuler")] + + def __init__(self, title, lines, danger_label): + super().__init__() + self._title = title + self._lines = lines + self._danger = danger_label + + def compose(self) -> ComposeResult: + with Vertical(id="confbox"): + yield Static(self._title, id="conftitle") + for line in self._lines: + yield Static(line) + with Horizontal(id="confbtns"): + yield Button("Annuler", variant="primary", id="c_no") + yield Button(self._danger, variant="error", id="c_yes") + + def on_button_pressed(self, event) -> None: + self.dismiss(event.button.id == "c_yes") + + def action_cancel(self) -> None: + self.dismiss(False) + + class VmActionsScreen(ModalScreen): + """Les opérations d'une VM, rassemblées en un seul endroit. + + Plutôt que trois touches de plus dans un pied de page qui en compte + déjà neuf : on voit ce qu'on va faire, et sur quelle machine.""" + + BINDINGS = [("escape", "cancel", "Fermer")] + + def __init__(self, vm): + super().__init__() + self._vm = vm + + def compose(self) -> ComposeResult: + with Vertical(id="actbox"): + yield Static( + f"Actions — {self._vm['name']} ({self._vm.get('ip') or '?'})", + id="acttitle", + ) + yield Static("Remettre à jour", classes="actgroup") + for key, label in UPDATE_PARTS: + yield Checkbox(label, value=True, id=f"u_{key}") + yield Button( + "Lancer la mise à jour", + variant="primary", + id="a_update", + ) + yield Static("Service", classes="actgroup") + yield Button("Redémarrer Odoo", id="a_restart") + yield Static("Irréversible", classes="actdanger") + yield Button( + "Supprimer la VM et ses disques", + variant="error", + id="a_delete", + ) + + def on_button_pressed(self, event) -> None: + if event.button.id == "a_update": + parts = [ + k + for k, _lbl in UPDATE_PARTS + if self.query_one(f"#u_{k}", Checkbox).value + ] + self.dismiss(("update", parts)) + elif event.button.id == "a_restart": + self.dismiss(("restart", [])) + elif event.button.id == "a_delete": + self.dismiss(("delete", [])) + + def action_cancel(self) -> None: + self.dismiss(None) + ICON = { "pending": "⏳", "running": "⏳", @@ -891,6 +1102,21 @@ def run_monitor(manifest_path: str, run_app: bool = True): } #errtitle { height: 1; color: $accent; text-style: bold; } #errlog { height: 1fr; border: solid $accent; } + VmActionsScreen { align: center middle; } + #actbox { + width: 62; height: auto; padding: 1 2; + border: thick $accent; background: $surface; + } + #acttitle { color: $accent; text-style: bold; padding-bottom: 1; } + .actgroup { color: $accent; text-style: bold; padding: 1 0 0 0; } + .actdanger { color: $error; text-style: bold; padding: 1 0 0 0; } + ConfirmScreen { align: center middle; } + #confbox { + width: 66; height: auto; padding: 1 2; + border: thick $error; background: $surface; + } + #conftitle { color: $error; text-style: bold; padding-bottom: 1; } + #confbtns { height: auto; padding-top: 1; } """ BINDINGS = [ ("q", "quit", "Quitter (détaché)"), @@ -900,6 +1126,7 @@ def run_monitor(manifest_path: str, run_app: bool = True): ("f", "follow", "Suivre"), ("c", "copy_log", "Copier log"), ("d", "details", "Détails erreurs"), + ("a", "vm_actions", "Actions VM"), ("p", "pause_all", "Pause tout"), ("o", "resume_all", "Reprendre tout"), ] @@ -1124,7 +1351,14 @@ def run_monitor(manifest_path: str, run_app: bool = True): continue ds = self._domstate.get(name) if ds == "gone": - self._final[name] = ("deleted", None, now - started) + # Même bornage que les autres états terminaux : une + # VM effacée ne « tourne » plus depuis la dernière + # ligne de son log. + self._final[name] = ( + "deleted", + None, + max(0.0, finished_at(vm["log"], now) - started), + ) # Icône seule (VM effacée) : évite « ❌ effacée » (10 # cellules) qui forçait une colonne État large. self._set_cell(table, name, "state", "🗑") @@ -1134,7 +1368,11 @@ def run_monitor(manifest_path: str, run_app: bool = True): continue state, code = st if state in ("done", "failed"): - elapsed = now - started + # Bornée à la dernière écriture du log, pas à l'instant + # présent : le suivi peut être rouvert bien plus tard. + elapsed = max( + 0.0, finished_at(vm["log"], now) - started + ) self._final[name] = (state, code, elapsed) # Les échecs sont enregistrés eux aussi (ok=False) : # sans eux, aucun taux de réussite n'est calculable. @@ -1179,7 +1417,8 @@ def run_monitor(manifest_path: str, run_app: bool = True): maxd = f" · max {self._fmt(max_dur)}" if max_dur else "" self.sub_title = ( f"{done}/{len(vms)} {t('completed')} · " - f"{self._fmt(now - started)}{eta}{maxd}" + f"{self._fmt(self._total_elapsed(now, started))}" + f"{eta}{maxd}" ) if tele: self.query_one("#telemetry", Static).update(tele) @@ -1516,6 +1755,157 @@ def run_monitor(manifest_path: str, run_app: bool = True): except (OSError, subprocess.SubprocessError): pass + # -- largeur des colonnes ---------------------------------------- # + COL_STEP = 2 + COL_MIN = 3 + COL_MAX = 60 + + def _cursor_column(self): + """Colonne sous le curseur, ou None. DataTable expose ses colonnes + dans un dictionnaire ORDONNÉ : le rang du curseur y donne la clé. + """ + table = self.query_one("#vms", DataTable) + keys = list(table.columns) + index = table.cursor_column + if not (0 <= index < len(keys)): + return None, None + return table, keys[index] + + def _apply_column_widths(self, table) -> None: + """Fait PRENDRE les largeurs à l'écran. + + « refresh(layout=True) » ne suffit pas, et c'est le piège : + mesuré sur Textual 8.2.8, la largeur de la colonne passe bien de + 22 à 34, mais la taille virtuelle du tableau reste à 81 — donc + rien ne bouge. « clear_cached_dimensions » et « refresh_column » + n'y changent rien non plus ; seul le recalcul des dimensions la + porte à 93. + + C'est une API privée, d'où le repli : une version future de + Textual dégradera l'ajustement au lieu de casser le suivi.""" + try: + table._update_dimensions(list(table.rows)) + except Exception: + table.refresh(layout=True) + + def _resize_column(self, delta) -> None: + table, key = self._cursor_column() + if key is None: + return + col = table.columns[key] + width = max( + self.COL_MIN, min(self.COL_MAX, (col.width or 0) + delta) + ) + if width == col.width: + # Butée atteinte : le SEUL cas où il faut le dire, puisque + # rien ne bougera à l'écran pour l'expliquer. + self.notify( + f"{col.label} : {width} (butee {self.COL_MIN}-{self.COL_MAX})", + severity="warning", + ) + return + col.width = width + # auto_width écraserait la largeur au prochain rendu : on la coupe, + # sinon le réglage ne survit pas à la première mise à jour. + col.auto_width = False + # Pas de notification quand ça marche : le changement se VOIT, et + # une bulle par frappe rendait l'ajustement pénible. + self._apply_column_widths(table) + + def action_col_grow(self) -> None: + self._resize_column(self.COL_STEP) + + def action_col_shrink(self) -> None: + self._resize_column(-self.COL_STEP) + + def action_col_reset(self) -> None: + """Rend aux colonnes les largeurs déclarées à la construction.""" + table = self.query_one("#vms", DataTable) + for key, width in COL_DEFAULT_WIDTHS.items(): + if key in table.columns: + table.columns[key].width = width + table.columns[key].auto_width = False + self._apply_column_widths(table) + + def _total_elapsed(self, now, started): + """Durée globale. Elle se FIGE quand plus rien ne tourne : sinon + le total continuait de courir sur un parc entièrement terminé, + et repartait de plus belle à chaque réouverture.""" + if len(self._final) < len(vms) or not self._final: + return now - started + return max(e for _s, _c, e in self._final.values()) + + def action_vm_actions(self) -> None: + """Ouvre les actions de la VM sélectionnée.""" + vm = self._vm_by_name(self._selected) + if not vm: + return + + def chosen(result): + if not result: + return + kind, parts = result + if kind == "update": + self._run_update(vm, parts) + elif kind == "restart": + self._run_in_vm( + vm, restart_odoo_cmd(), "Redemarrage d'Odoo" + ) + elif kind == "delete": + self._ask_delete(vm) + + self.push_screen(VmActionsScreen(vm), chosen) + + def _run_in_vm(self, vm, cmd, title) -> None: + """Exécute une commande DANS la VM, terminal rendu. + + « suspend() » comme pour SSH et la console : sudo peut demander son + mot de passe et la sortie est longue ; la garder derrière Textual + la rendrait illisible. La pause finale évite que l'écran reparte + avant qu'on ait lu le résultat.""" + if not vm.get("ip"): + self.notify("Pas d'IP pour cette VM.", severity="error") + return + with self.suspend(): + print(f"\n=== {title} — {vm['name']} ===") + os.system( + f"ssh {SSH_OPTS} erplibre@{vm['ip']} " + f"{shlex.quote(cmd)} || true" + ) + input("\nEntrée pour revenir au suivi… ") + + def _run_update(self, vm, parts) -> None: + cmd = update_remote_cmd(parts) + if not cmd: + self.notify("Rien de coché.", severity="warning") + return + self._run_in_vm(vm, cmd, "Mise a jour") + + def _ask_delete(self, vm) -> None: + """Suppression : jamais sans une seconde main.""" + + def confirmed(yes): + if not yes: + return + with self.suspend(): + print(f"\n=== Suppression — {vm['name']} ===") + os.system(delete_vm_cmd(vm["name"], True) + " || true") + input("\nEntrée pour revenir au suivi… ") + + self.push_screen( + ConfirmScreen( + f"Supprimer {vm['name']} ?", + [ + "La VM est arrêtée, sa définition retirée,", + "et son disque qcow2 EFFACÉ. Rien n'est récupérable.", + "", + f" /var/lib/libvirt/images/{vm['name']}.qcow2", + ], + "Supprimer définitivement", + ), + confirmed, + ) + def action_pause_all(self) -> None: """Met en PAUSE (virsh suspend) toutes les VM en cours d'exécution. L'install reprend là où elle en était après « Reprendre ».""" From e60f18806e8bc6e6da364b32ba03b1848b664177 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 12 Aug 2026 03:58:58 -0400 Subject: [PATCH 28/41] [ADD] qemu monitor: VM architecture, scrolling and resizable columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The architecture was missing, though it alone explains why an install takes ten times longer: s390x and arm64 are EMULATED on an amd64 host. Without it you look for the fault elsewhere. An "Arch" column, right after the name. The table was pinned at 74 columns: beyond that nothing was reachable and no scrollbar showed, for want of reserved space. Automatic width capped at 60% of the screen, scrolling both ways, and VISIBLE bars rather than guessable ones. Columns can finally be adjusted: "+" widens the cursor's, "-" narrows it, "0" returns them all to their original width -- the same keys as the mail TUI. auto_width is turned off along the way, otherwise the setting did not survive the table's first update, and the resize aimed at the "#" column whatever the cursor was on. --- FR --- L'architecture manquait, alors qu'elle explique à elle seule pourquoi une installation dure dix fois plus : s390x et arm64 sont ÉMULÉES sur un hôte amd64. Sans elle, on cherche la faute ailleurs. Une colonne « Arch », juste après le nom. Le tableau était figé à 74 colonnes : au-delà rien n'était atteignable et aucune barre de défilement n'apparaissait, faute de place réservée. Largeur automatique plafonnée à 60 % de l'écran, défilement dans les deux sens, et barres VISIBLES plutôt que devinables. Les colonnes s'ajustent enfin : « + » élargit celle du curseur, « - » la rétrécit, « 0 » les ramène toutes à leur largeur d'origine — les mêmes touches que la TUI courriel. auto_width est désactivé au passage, sans quoi le réglage ne survivait pas à la première mise à jour du tableau, et le redimensionnement visait la colonne « # » quel que soit le curseur. Assisted-by: Claude Opus 5 --- script/todo/qemu_install_monitor.py | 100 ++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/script/todo/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index 9d33b2a..eb63c5c 100644 --- a/script/todo/qemu_install_monitor.py +++ b/script/todo/qemu_install_monitor.py @@ -842,6 +842,22 @@ def virsh_domstates() -> dict: # --------------------------------------------------------------------------- # # Dashboard Textual # --------------------------------------------------------------------------- # +# Largeurs de colonnes du tableau de suivi. Une seule source : la +# construction du tableau les lit ici, et « 0 » y revient. Les redéclarer à +# deux endroits, c'est garantir qu'un jour la remise à zéro rendra autre chose +# que ce qui était affiché au départ. +COL_DEFAULT_WIDTHS = { + "seq": 3, + "vm": 22, + "arch": 7, + "err": 4, + "state": 8, + "odoo": 6, + "elapsed": 7, + "disk": 8, +} + + # Parties d'une remise à jour, dans l'ordre où elles doivent tourner : les # paquets système d'abord (compilateurs et en-têtes), le code ensuite, les # dépendances Python en dernier — elles se compilent contre les deux premiers. @@ -1089,7 +1105,17 @@ def run_monitor(manifest_path: str, run_app: bool = True): class Monitor(App): CSS = """ - DataTable { width: 74; height: 1fr; overflow-x: auto; border: solid $accent; } + /* « width: 74 » figeait la table : au-delà, les colonnes élargies + n'étaient plus atteignables, et la barre horizontale ne s'affichait + pas faute de place réservée. « scrollbar-size » la rend VISIBLE plutôt + que devinable, et « max-width » laisse la table suivre l'élargissement + des colonnes sans manger tout l'écran. */ + DataTable { + width: auto; max-width: 60%; height: 1fr; + overflow-x: auto; overflow-y: auto; + scrollbar-size-horizontal: 1; scrollbar-size-vertical: 1; + border: solid $accent; + } RichLog { border: solid $accent; } #telemetry { height: 1; color: $text-muted; } #stats { height: 1; color: $accent; } @@ -1127,6 +1153,13 @@ def run_monitor(manifest_path: str, run_app: bool = True): ("c", "copy_log", "Copier log"), ("d", "details", "Détails erreurs"), ("a", "vm_actions", "Actions VM"), + # Mêmes touches que la TUI mail, qui redimensionne ses volets : + # « + » élargit, « - » rétrécit, « 0 » remet tout d'aplomb. + ("plus", "col_grow", "Colonne +"), + ("minus", "col_shrink", "Colonne -"), + ("0", "col_reset", "Colonnes par défaut"), + ("less_than_sign", "col_prev", "Colonne précédente"), + ("greater_than_sign", "col_next", "Colonne suivante"), ("p", "pause_all", "Pause tout"), ("o", "resume_all", "Reprendre tout"), ] @@ -1134,6 +1167,9 @@ def run_monitor(manifest_path: str, run_app: bool = True): def __init__(self): super().__init__() self._offsets = {vm["name"]: 0 for vm in vms} + # Colonne visee par « + » / « - » : le nom de VM, celle qu'on a + # vraiment besoin d'elargir. « < » et « > » la deplacent. + self._col_target = list(COL_DEFAULT_WIDTHS).index("vm") self._selected = vms[0]["name"] if vms else None self._follow = True # Statuts TERMINAUX mémorisés : une VM finie n'est plus relue @@ -1183,20 +1219,35 @@ def run_monitor(manifest_path: str, run_app: bool = True): # tronqué (« ❌ effacée », « ⏸ en pause » lisibles) ; la table # défile horizontalement (overflow-x) pour les noms de VM longs. # « # » : numéro de séquence de la VM (première colonne). - table.add_column("#", key="seq", width=3) - table.add_column("VM", key="vm", width=22) - table.add_column("⚠", key="err", width=4) + table.add_column("#", key="seq", width=COL_DEFAULT_WIDTHS["seq"]) + table.add_column("VM", key="vm", width=COL_DEFAULT_WIDTHS["vm"]) + # L'architecture explique à elle seule qu'une installation dure + # dix fois plus longtemps : s390x et arm64 sont ÉMULÉES sur un + # hôte amd64. La voir évite de chercher la panne ailleurs. + table.add_column( + "Arch", key="arch", width=COL_DEFAULT_WIDTHS["arch"] + ) + table.add_column("⚠", key="err", width=COL_DEFAULT_WIDTHS["err"]) # width=8 : le plus long libellé restant est « ⏸ pause » (7) — # « effacée » (10) est remplacé par l'icône 🗑 (voir plus bas). - table.add_column("État", key="state", width=8) + table.add_column( + "État", key="state", width=COL_DEFAULT_WIDTHS["state"] + ) # « Odoo » : l'UI web répond-elle sur :8069 ? (🟢 up / — down) - table.add_column("Odoo", key="odoo", width=6) - table.add_column("Durée", key="elapsed", width=7) - table.add_column("Disque", key="disk", width=8) + table.add_column( + "Odoo", key="odoo", width=COL_DEFAULT_WIDTHS["odoo"] + ) + table.add_column( + "Durée", key="elapsed", width=COL_DEFAULT_WIDTHS["elapsed"] + ) + table.add_column( + "Disque", key="disk", width=COL_DEFAULT_WIDTHS["disk"] + ) for i, vm in enumerate(vms, 1): table.add_row( str(i), vm["name"], + vm.get("arch") or "?", "", "⏳", "—", @@ -1761,14 +1812,20 @@ def run_monitor(manifest_path: str, run_app: bool = True): COL_MAX = 60 def _cursor_column(self): - """Colonne sous le curseur, ou None. DataTable expose ses colonnes - dans un dictionnaire ORDONNÉ : le rang du curseur y donne la clé. - """ + """Colonne VISEE par le redimensionnement. + + Surtout pas « table.cursor_column » : le curseur est en mode + LIGNE, sa colonne reste donc a 0 quoi qu'on fasse. Chaque « + » ou + « - » tombait sur « # », large de 3 et deja a sa butee — d'ou un + avertissement en boucle et aucune colonne atteignable. + + La cible se deplace avec « < » et « > », et part sur le nom de VM, + la seule qu'on ait vraiment besoin d'elargir.""" table = self.query_one("#vms", DataTable) keys = list(table.columns) - index = table.cursor_column - if not (0 <= index < len(keys)): + if not keys: return None, None + index = max(0, min(self._col_target, len(keys) - 1)) return table, keys[index] def _apply_column_widths(self, table) -> None: @@ -1812,6 +1869,23 @@ def run_monitor(manifest_path: str, run_app: bool = True): # une bulle par frappe rendait l'ajustement pénible. self._apply_column_widths(table) + def _move_col_target(self, delta) -> None: + """Deplace la cible, en boucle sur les colonnes.""" + table = self.query_one("#vms", DataTable) + keys = list(table.columns) + if not keys: + return + self._col_target = (self._col_target + delta) % len(keys) + self.notify( + f"Colonne visee : {table.columns[keys[self._col_target]].label}" + ) + + def action_col_prev(self) -> None: + self._move_col_target(-1) + + def action_col_next(self) -> None: + self._move_col_target(1) + def action_col_grow(self) -> None: self._resize_column(self.COL_STEP) From d35950ab97346e1be333578cc832784153181076 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 00:33:04 -0400 Subject: [PATCH 29/41] [ADD] tui qemu: freeze one VM's resources, row in green MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A 🔒 box at the head of each row. Ticked, the VM's four current values -- vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile no longer reaches it. Unticked, they are removed and the VM falls back under the profile. The lock shows on the WHOLE ROW, not in a box lost at the end: that is what lets you scan the plan and see at once what escapes the profile. It rests on the override mechanism already proven, indexed by catalog identity, so it survives a remount. Locked state stays distinct from overrides: a VM can be edited without being frozen, and the lock covers all four fields at once. Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving the card an id broke the remount, the old one still being there. Lists kept stale values across a remount, and a refresh took back the free entry. A frozen VM changed version all the same. And switching profile wiped the disk size that had been set. --- FR --- Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges : le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une case perdue au bout : c'est ce qui permet de balayer le plan et de savoir d'un coup ce qui échappe au profil. Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par identité de catalogue : il survit donc à un remontage. L'état verrouillé reste distinct des surcharges : une VM peut être modifiée sans être figée, et le verrou couvre les quatre champs d'un coup. Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE : donner un id à la carte faisait échouer le remontage, l'ancienne étant encore là. Les listes gardaient des valeurs périmées au remontage, et un rafraîchissement reprenait la saisie libre. Une VM figée changeait quand même de version. Et changer de profil effaçait la taille de disque réglée. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 291 +++++++++++++++++++++++++++++++- 1 file changed, 285 insertions(+), 6 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index b7ad1fe..7e32146 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -388,8 +388,7 @@ def run_deploy_form(ctx, run_app: bool = True): /* La branche porte des noms longs (« 1.6.0 », « develop », « feature/xyz ») : trop étroite, la liste les tronque et on ne sait plus ce qu'on a choisi. */ - - + .vmbranch { width: 18; } .vmcopy { width: 5; min-width: 5; } .vmhead { height: 1; } .vmrow { height: 3; align-vertical: middle; } @@ -694,7 +693,9 @@ def run_deploy_form(ctx, run_app: bool = True): # vit sur la ligne d'affichage, jamais sur la VM : celle-ci part # telle quelle dans la spec, que la CLI produit à l'identique. for row, entry in zip(self.rows, entries): - row["custom"] = bool(self.overrides.get(entry_key(entry))) + key = entry_key(entry) + row["custom"] = bool(self.overrides.get(key)) + row["locked"] = key in self.locked self._render_plan() self._render_mise() self._render_store() @@ -803,7 +804,19 @@ def run_deploy_form(ctx, run_app: bool = True): widgets = [] for i, r in enumerate(self.rows): vm = r["vm"] + key = entry_key(self._selected_entries()[i]) row = Horizontal( + # « + » ajoute un exemplaire de CETTE entrée ; « - » ne + # s'affiche que sur une copie, pour qu'on ne puisse pas + # retirer l'original par mégarde. + Button("+", id=f"p{i}", classes="vmcopy"), + Button("✎", id=f"r{i}", classes="vmcopy"), + Button( + "🔒" if key in self.locked else "🔓", + id=f"l{i}", + variant="success" if key in self.locked else "default", + classes="vmlock", + ), Select( [(str(c), c) for c in ctx["cpu_presets"]] + [(t("free value…"), FREE)], @@ -867,6 +880,22 @@ def run_deploy_form(ctx, run_app: bool = True): id=f"c{i}_disk", classes="freeval", ), + ( + Button("−", id=f"m{i}", classes="vmcopy") + if item.get("instance") + else Static("", classes="vmcopy") + ), + Select( + [(b, b) for b in branches], + classes="vmbranch", + value=( + item.get("branch") + or self.rows[i]["vm"].get("branch") + or branches[0] + ), + allow_blank=False, + id=f"v{i}_branch", + ), Select( self._type_options(), value=vm.get("desktop") or SERVER, @@ -879,7 +908,15 @@ def run_deploy_form(ctx, run_app: bool = True): Vertical( Static(self._row_head(i, r), id=f"h{i}"), row, - classes="vmcard", + # Pas d'id sur la carte : « remove_children() » est + # ASYNCHRONE, les anciennes sont encore là au montage + # et Textual refuse deux frères de même id. Les ids + # des champs vivent un niveau plus bas, dans un parent + # neuf — la collision ne les touche pas. On atteint + # donc la carte par son RANG. + classes=( + "vmcard locked" if key in self.locked else "vmcard" + ), ) ) @@ -910,6 +947,50 @@ def run_deploy_form(ctx, run_app: bool = True): self._sync_free_inputs() self._syncing = False + def _refresh_row_widgets(self) -> None: + """Remet les listes de chaque rangée sur ce que la VM vaut MAINTENANT. + + Sans cela, changer le profil x1..x4 mettait les totaux à jour mais + laissait les listes sur leurs anciennes valeurs : l'écran affichait + 8192 pendant que la VM valait 4096. Les rangées ne sont remontées + que si le JEU de VM change — pour ne pas voler le focus — donc ce + rafraîchissement doit se faire à la main. + + Aucun risque de boucle : on_select_changed ignore une valeur déjà + égale à celle du modèle, et le modèle vient précisément d'être + recalculé.""" + for i, r in enumerate(self.rows): + vm = r["vm"] + for field, presets in ( + ("vcpus", ctx["cpu_presets"]), + ("ram", ctx["ram_presets"]), + ("disk", ctx["disk_presets"]), + ): + try: + sel = self.query_one(f"#v{i}_{field}", Select) + except Exception: + continue + # Une liste posée sur « libre… » ne doit PAS être remise + # sur une valeur : l'utilisateur vient de la choisir, et + # tant qu'il n'a rien tapé la VM vaut encore celle du + # profil — on la lui reprendrait sous les doigts. + if sel.value is FREE: + continue + # Une valeur libre déjà saisie n'est dans aucune liste : + # liste vide, la saisie à côté porte le nombre. + sel.value = ( + vm[field] if vm[field] in presets else SELECT_NULL + ) + for wid, value in ( + (f"#v{i}_type", vm.get("desktop") or SERVER), + (f"#v{i}_branch", vm.get("branch") or self._branch()), + ): + try: + self.query_one(wid, Select).value = value + except Exception: + pass + self._sync_free_inputs() + def _sync_free_inputs(self) -> None: for i, r in enumerate(self.rows): vm = r["vm"] @@ -922,7 +1003,16 @@ def run_deploy_form(ctx, run_app: bool = True): widget = self.query_one(f"#c{i}_{field}", Input) except Exception: continue - free = vm[field] not in presets + # Visible si la valeur EST libre, ou si la liste est + # posée sur « libre… » en attente d'une saisie. + try: + chosen_free = ( + self.query_one(f"#v{i}_{field}", Select).value + is FREE + ) + except Exception: + chosen_free = False + free = chosen_free or vm[field] not in presets widget.display = free widget.disabled = not free @@ -933,7 +1023,12 @@ def run_deploy_form(ctx, run_app: bool = True): vm = row["vm"] icon = {"new": "", "exists": "⏭ ", "orphan": "❌ "}[row["state"]] state = "" if row["state"] == "new" else f" {icon}{row['note']}" - mark = " ✎" if row.get("custom") else "" + if row.get("locked"): + mark = " 🔒 figée" + elif row.get("custom"): + mark = " ✎" + else: + mark = "" return ( f"[b]{vm['name']}[/b] {vm['distro']} {vm['version']} " f"[{vm['arch']}] {row['disk_gb']}G{state}{mark}" @@ -953,6 +1048,7 @@ def run_deploy_form(ctx, run_app: bool = True): ) except Exception: pass + self._refresh_row_widgets() if not self.rows: # Rien de coché : un total à zéro n'apprend rien, on dit # plutôt comment remplir la liste. @@ -995,12 +1091,23 @@ def run_deploy_form(ctx, run_app: bool = True): self.arch = arches[event.radio_set.pressed_index] self._reload_catalog() elif event.radio_set.id == "f_type": + self._clear_overrides(("desktop",)) # Recalcul : le disque annonce inclut le bureau, et la # colonne Statut affiche le type de VM. self._recompute() elif event.radio_set.id == "f_profile": index = event.radio_set.pressed_index self.profile = "custom" if index == 4 else str(index + 1) + # Un multiplicateur x1..x4 ne touche QUE les vCPU et la RAM — + # apply_profile y laisse le disque du catalogue. Y effacer une + # taille de disque réglée à la main la faisait disparaître sans + # rien mettre à la place : on revenait à 20G sans l'avoir + # demandé. Le disque n'est rendu au commun que par le profil + # « personnalisé », qui en porte un. + fields = ("vcpus", "ram") + if self.profile == "custom": + fields += ("disk",) + self._clear_overrides(fields) custom = self.profile == "custom" for field, (sel, _inp) in RES_FIELDS.items(): self.query_one(sel, Select).disabled = not custom @@ -1027,6 +1134,36 @@ def run_deploy_form(ctx, run_app: bool = True): entries = self._selected_entries() return entry_key(entries[index]) if index < len(entries) else None + def _clear_overrides(self, fields) -> None: + """Rend au choix commun les VM NON figées, pour ces champs-là. + + Le cadenas est la seule chose qui résiste. Une valeur réglée à la + main sur une rangée cède donc au choix global suivant : c'est ce + qu'on attend d'un réglage « général », et le verrou existe + précisément pour dire « pas celle-ci ». + + Par champ, pas en bloc : changer la RAM générale n'a aucune raison + d'effacer le disque qu'on a réglé sur une VM. + + « name » n'y figure jamais : un renommage est explicite et ne + découle d'aucune valeur générale.""" + changed = False + for key in list(self.overrides): + if key in self.locked: + continue + for field in fields: + changed |= self.overrides[key].pop(field, None) is not None + if not self.overrides[key]: + self.overrides.pop(key, None) + if changed: + # Forcer le remontage : une rangée peut porter une saisie + # LIBRE, que le simple rafraîchissement laisse en place — on + # verrait « 12 » à l'écran pendant que la VM vaut 2. Le + # remontage rebâtit tout depuis le modèle. Sans risque de vol + # de focus : ce chemin part d'un widget GLOBAL, jamais d'une + # rangée. + self._shown_ids = () + def _set_override(self, index, field, value) -> None: """Écrit — ou retire — la surcharge d'UNE VM.""" key = self._row_key(index) @@ -1121,6 +1258,19 @@ def run_deploy_form(ctx, run_app: bool = True): self._set_override(index, field, event.value) self._recompute() return + # Les choix GLOBAUX de branche et de profil ne portent aucune + # valeur de ressource : ils tombaient donc dans le « return » + # ci-dessous sans rien recalculer, et les rangées restaient sur + # l'ancienne version. Elles n'en gardent pas de copie — « » y + # veut dire « celle du formulaire » — il suffit de redessiner. + if event.select.id in ("f_branch", "f_profile_install"): + self._clear_overrides( + ("branch",) + if event.select.id == "f_branch" + else ("install_cmd",) + ) + self._recompute() + return field = SELECT_TO_FIELD.get(event.select.id) if not field: return @@ -1134,6 +1284,7 @@ def run_deploy_form(ctx, run_app: bool = True): self._free[field] = False self._show_free(field, False) self.custom[field] = event.value + self._clear_overrides((field,)) self._recompute() def _apply_free(self, field) -> None: @@ -1146,6 +1297,7 @@ def run_deploy_form(ctx, run_app: bool = True): self.custom[field] = parse_ram(raw) else: self.custom[field] = positive_int(raw, 0) + self._clear_overrides((field,)) def on_input_changed(self, event) -> None: if self._syncing: @@ -1166,6 +1318,133 @@ def run_deploy_form(ctx, run_app: bool = True): self._apply_free(field) self._recompute() + def _set_lock(self, index, on) -> None: + """Fige — ou libère — les ressources d'une VM. + + Figer, c'est recopier les valeurs EFFECTIVES du moment dans les + surcharges : le profil commun ne les atteint plus. Libérer les + retire, et la VM retombe sous le profil. Le mécanisme est celui + des surcharges, déjà éprouvé ; le verrou n'en est que la commande + explicite, et il couvre les quatre champs d'un coup.""" + key = self._row_key(index) + if key is None or index >= len(self.rows): + return + if on: + vm = self.rows[index]["vm"] + self.locked.add(key) + # TOUT ce que la VM tient d'un choix commun est recopié, pas + # seulement les ressources : la branche et le profil Odoo en + # font partie. Les oublier laissait une VM « figée » changer + # de version d'ERPLibre dès qu'on touchait au choix générique, + # ce qui vide le mot de son sens. + # + # Les deux se résolvent AVANT d'être figés : « » y signifie + # « celle du formulaire », et geler une chaîne vide ne + # gèlerait rien du tout. + self.overrides[key] = { + "vcpus": vm["vcpus"], + "ram": vm["ram"], + "disk": vm["disk"], + "desktop": vm.get("desktop") or "", + "branch": vm.get("branch") or self._branch(), + "install_cmd": ( + vm.get("install_cmd") or self._profile_cmd() + ), + } + else: + self.locked.discard(key) + self.overrides.pop(key, None) + self._recompute() + # La couleur de la ligne suit le verrou sans tout remonter : un + # remontage volerait le focus à la case qu'on vient de cocher. + cards = self.query_one("#plan", VerticalScroll).children + if index < len(cards): + cards[index].set_class(on, "locked") + btn = self.query_one(f"#l{index}", Button) + btn.label = "🔒" if on else "🔓" + btn.variant = "success" if on else "default" + + def _add_copy(self, index, delta) -> None: + """Ajoute ou retire un exemplaire de l'entrée visée. + + Retirer enlève le DERNIER exemplaire, et avec lui ses réglages : + les garder ferait resurgir d'anciennes valeurs à la copie + suivante, sans que rien ne l'explique.""" + entries = self._plan_entries() + if index >= len(entries): + return + item = entries[index] + base = (item["distro"], item["version"], item["arch"]) + count = self.copies.get(base, 0) + if delta > 0: + self.copies[base] = count + 1 + else: + if count <= 0: + return + gone = (*base, count) + self.overrides.pop(gone, None) + self.locked.discard(gone) + self.copies[base] = count - 1 + if not self.copies[base]: + self.copies.pop(base, None) + self._recompute() + # Le JEU de VM a changé : les rangées doivent être rebâties. + self._mount_rows() + + def on_button_pressed(self, event) -> None: + match = re.match(r"([pm])(\d+)$", event.button.id or "") + if match: + self._add_copy( + int(match.group(2)), 1 if match.group(1) == "p" else -1 + ) + return + match = re.match(r"r(\d+)$", event.button.id or "") + if match: + self._rename(int(match.group(1))) + return + match = re.match(r"l(\d+)$", event.button.id or "") + if match: + index = int(match.group(1)) + key = self._row_key(index) + if key is not None: + self._set_lock(index, key not in self.locked) + + def _rename(self, index) -> None: + """Renomme une VM. Le nom saisi devient une surcharge comme les + autres : il survit au recalcul, et F4 le retire avec le reste.""" + key = self._row_key(index) + if key is None or index >= len(self.rows): + return + entries = self._plan_entries() + auto = vm_name( + entries[index]["name"], + self.rows[index]["vm"].get("desktop"), + desktop_suffixes, + ) + + def done(value): + if value is None: + return + if not str(value).strip(): + self.overrides.get(key, {}).pop("name", None) + if not self.overrides.get(key): + self.overrides.pop(key, None) + else: + clean = clean_hostname(value) + if not clean: + self.notify( + t("Invalid name: letters, digits, hyphens."), + severity="error", + ) + return + self.overrides.setdefault(key, {})["name"] = clean + self._recompute() + self._mount_rows() + + self.push_screen( + RenameScreen(self.rows[index]["vm"]["name"], auto), done + ) + def on_checkbox_changed(self, event) -> None: if event.checkbox.id == "f_install": self._recompute() # le disque annoncé inclut le +5 G ERPLibre From cbdded8fabeefc61af1b56665afff2031cef2455 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 00:37:45 -0400 Subject: [PATCH 30/41] [ADD] tui qemu: deploy several copies of one entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A "+" per row adds a copy of the entry, a "−" removes it. The "−" only appears on a copy: the original cannot be deleted by mistake, it is unticked in the catalogue. The name adapts on its own -- erplibre-ubuntu-2404, then -2, then -3. The first keeps the catalogue name, so existing deployments keep theirs. The real work is in identity. entry_key was (distro, version, arch): two copies shared it, so setting the first set the second and their locks fought each other. It now carries the instance number, and overrides as well as locks follow the right machine. Each instance is a COPY of the dictionary, never a shared reference. Removing a copy forgets its settings: keeping them would resurrect old values on the next copy, with nothing to explain it. --- FR --- Un « + » par rangée ajoute une copie de l'entrée, un « − » la retire. Le « − » n'apparaît que sur une copie : l'original ne se supprime pas par mégarde, il se décoche au catalogue. Le nom s'adapte seul — erplibre-ubuntu-2404, puis -2, puis -3. Le premier garde le nom du catalogue, pour que les déploiements existants gardent le leur. Le vrai travail est dans l'identité. entry_key valait (distro, version, arch) : deux copies la partageaient, donc régler la première réglait la seconde et leurs verrous se marchaient dessus. Elle porte maintenant le numéro d'exemplaire, et surcharges comme verrous suivent la bonne machine. Chaque exemplaire est une COPIE du dictionnaire, jamais une référence partagée. Retirer une copie oublie ses réglages : les garder ferait resurgir d'anciennes valeurs à la copie suivante, sans que rien ne l'explique. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 51 ++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 7e32146..568b494 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -36,9 +36,40 @@ except Exception: # pragma: no cover - repli si i18n indisponible # Logique pure — aucune dépendance à Textual, testable telle quelle # --------------------------------------------------------------------------- # def entry_key(entry) -> tuple: - """Identité stable d'une entrée de catalogue, indépendante de son rang : - les surcharges par VM y survivent quand la sélection change.""" - return (entry["distro"], entry["version"], entry["arch"]) + """Identité stable d'une VM du plan, indépendante de son rang d'affichage : + surcharges et verrous y survivent quand la sélection change. + + Le quatrième membre est le numéro d'EXEMPLAIRE. Sans lui, deux copies de + la même entrée de catalogue partageaient une identité : régler la première + réglait la seconde, et les verrous se marchaient dessus.""" + return ( + entry["distro"], + entry["version"], + entry["arch"], + entry.get("instance", 0), + ) + + +def copy_name(base: str, instance: int) -> str: + """Nom du n-ième exemplaire. Le premier garde le nom du catalogue, pour + que les déploiements d'avant gardent le leur.""" + return base if not instance else f"{base}-{instance + 1}" + + +def expand_copies(entries, copies): + """[entrée] + {clé de base: exemplaires en plus} -> [entrée par VM]. + + Chaque exemplaire est une COPIE du dictionnaire, avec son numéro et son + nom : rien n'est partagé, donc régler l'un ne touche pas l'autre.""" + out = [] + for e in entries: + base = (e["distro"], e["version"], e["arch"]) + for i in range(1 + max(0, (copies or {}).get(base, 0))): + item = dict(e) + item["instance"] = i + item["name"] = copy_name(e["name"], i) + out.append(item) + return out def parse_disk(value): @@ -273,6 +304,7 @@ def run_deploy_form(ctx, run_app: bool = True): from textual.containers import Horizontal, Vertical, VerticalScroll from textual.screen import ModalScreen from textual.widgets import ( + Button, Checkbox, Footer, Header, @@ -629,6 +661,10 @@ def run_deploy_form(ctx, run_app: bool = True): self._reload_catalog(first_load=True) # -- catalogue et recalcul ------------------------------------- # + def _plan_entries(self): + """Entrées du plan : la sélection, dépliée en exemplaires.""" + return expand_copies(self._selected_entries(), self.copies) + def _entries(self): return catalog.get(self.arch, []) @@ -665,7 +701,7 @@ def run_deploy_form(ctx, run_app: bool = True): return [entries[i] for i in sorted(widget.selected)] def _recompute(self): - entries = self._selected_entries() + entries = self._plan_entries() self.vms = build_vms( entries, self.profile, @@ -780,7 +816,7 @@ def run_deploy_form(ctx, run_app: bool = True): """Identité du JEU de VM affiché. Reconstruire les widgets à chaque frappe ferait perdre le focus en pleine saisie : on ne le fait que si la liste elle-même a changé.""" - return tuple(entry_key(e) for e in self._selected_entries()) + return tuple(entry_key(e) for e in self._plan_entries()) def _type_options(self): return [(t("Server"), SERVER)] + [ @@ -804,7 +840,8 @@ def run_deploy_form(ctx, run_app: bool = True): widgets = [] for i, r in enumerate(self.rows): vm = r["vm"] - key = entry_key(self._selected_entries()[i]) + item = self._plan_entries()[i] + key = entry_key(item) row = Horizontal( # « + » ajoute un exemplaire de CETTE entrée ; « - » ne # s'affiche que sur une copie, pour qu'on ne puisse pas @@ -1131,7 +1168,7 @@ def run_deploy_form(ctx, run_app: bool = True): return index if 0 <= index < len(self.rows) else None def _row_key(self, index): - entries = self._selected_entries() + entries = self._plan_entries() return entry_key(entries[index]) if index < len(entries) else None def _clear_overrides(self, fields) -> None: From 17d0d607415da1e42e0308a06db0ef9514e794e0 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 00:41:16 -0400 Subject: [PATCH 31/41] [ADD] tui qemu: rename a VM with the pencil MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A "✎" per row opens an input prefilled with the current name. The given name becomes an override like any other: it survives recomputation, and F4 removes it along with the VM's other settings. An explicit name makes automatic naming GIVE WAY. Switching the VM to GNOME no longer appends "-gnome": adding a suffix would amount to correcting the user. Clearing the field restores the automatic name, copy and desktop suffixes included. The name becomes the machine's HOSTNAME, so it is validated against RFC 1123 before being accepted. One capital or one dot too many and cloud-init silently ignores it -- the VM would stay "ubuntu", and you would find out once deployed. Capitals are lowered, the rest is refused with a message. --- FR --- Un « ✎ » par rangée ouvre une saisie préremplie du nom courant. Le nom donné devient une surcharge comme les autres : il survit au recalcul, et F4 le retire avec le reste des réglages de la VM. Un nom explicite fait CÉDER le nommage automatique. Passer la VM en GNOME n'y colle plus « -gnome » : y ajouter un suffixe reviendrait à corriger l'utilisateur. Vider le champ rend le nom automatique, suffixes de copie et de bureau compris. Le nom devient le NOM D'HÔTE de la machine : il est donc validé en RFC 1123 avant d'être retenu. Une majuscule ou un point de trop et cloud-init l'ignore en silence — la VM resterait « ubuntu », et on le découvrirait une fois déployée. Les majuscules sont abaissées, le reste est refusé avec un message. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 60 ++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 568b494..e982af9 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -180,6 +180,20 @@ def apply_overrides(vms, entries, overrides): return vms +def clean_hostname(value): + """Nom d'hote valide (RFC 1123) tire de la saisie, ou None. + + Le nom d'une VM devient son NOM D'HOTE : une majuscule ou un point de + trop et cloud-init l'ignore en silence, la machine reste « ubuntu ». + Mieux vaut refuser ici que le decouvrir sur une VM deja deployee.""" + txt = str(value or "").strip().lower() + if not txt or len(txt) > 63: + return None + if not re.fullmatch(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?", txt): + return None + return txt + + def vm_name(base, desktop, suffixes): """Nom de VM, suffixé du bureau quand il y en a un. @@ -211,7 +225,11 @@ def build_vms( ) # APRÈS les surcharges : c'est là seulement que le type de chaque VM est # connu, puisqu'il se choisit machine par machine. - for vm in vms: + for vm, e in zip(vms, entries): + if (overrides or {}).get(entry_key(e), {}).get("name"): + # Nom donne a la main : il gagne, sans suffixe ajoute. Y coller + # « -gnome » reviendrait a corriger l'utilisateur. + continue vm["name"] = vm_name(vm["name"], vm.get("desktop"), suffixes) return vms @@ -368,6 +386,38 @@ def run_deploy_form(ctx, run_app: bool = True): f"RAM≥{e['ram']}Mo {e['disk']}" ) + class RenameScreen(ModalScreen): + """Nom d'une VM. Vide = revenir au nom automatique.""" + + BINDINGS = [("escape", "cancel", t("Cancel"))] + + def __init__(self, name, auto): + super().__init__() + self._name = name + self._auto = auto + + def compose(self) -> ComposeResult: + with Vertical(id="renbox"): + yield Static(t("Rename the VM"), id="rentitle") + yield Input(value=self._name, id="renval") + yield Static( + f" {t('Empty = back to the automatic name:')} " + f"{self._auto}", + id="renhint", + ) + with Horizontal(id="renbtns"): + yield Button(t("Cancel"), id="ren_no") + yield Button(t("Rename"), variant="primary", id="ren_ok") + + def on_button_pressed(self, event) -> None: + if event.button.id != "ren_ok": + self.dismiss(None) + return + self.dismiss(self.query_one("#renval", Input).value) + + def action_cancel(self) -> None: + self.dismiss(None) + class PreviewScreen(ModalScreen): """Aperçu des commandes qui seraient lancées (aucune exécution).""" @@ -427,6 +477,14 @@ def run_deploy_form(ctx, run_app: bool = True): .vmrow Select { width: 15; } .vmrow Input { width: 11; } #reslabel { color: $text-muted; } + RenameScreen { align: center middle; } + #renbox { + width: 60; height: auto; padding: 1 2; + border: thick $accent; background: $surface; + } + #rentitle { color: $accent; text-style: bold; } + #renhint { color: $text-muted; } + #renbtns { height: auto; padding-top: 1; } PreviewScreen { align: center middle; } #prevbox { width: 90%; height: 70%; padding: 1 2; From 2eee924759bfd13f4f055c668fbd0380601bb06c Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 00:46:12 -0400 Subject: [PATCH 32/41] [ADD] tui qemu: an ERPLibre version per VM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A branch list per row. The form's branch stays the default; picking another for a VM creates an override, going back clears it -- the VM then follows the form again, which is what one expects when restoring the common choice. It reaches all the way down to execution, like VM type before it. "branch" now takes either a string for the whole fleet or a {name: branch} map, and the remote command is built per machine as soon as branches differ. The ground was ready: launch_installs already knows how to take one command per VM. A VM can therefore carry develop while another validates 1.6.0, in the same deployment and the same monitoring table. One trap: a row's branch fell back to develop on remount, the list being rebuilt from the form rather than from the override it already held. --- FR --- Une liste de branches par rangée. La branche du formulaire reste le défaut ; en choisir une autre pour une VM crée une surcharge, y revenir l'efface — la VM suit alors de nouveau le formulaire, ce qui est le plus attendu quand on remet le choix commun. Elle descend jusqu'à l'exécution, comme le type de VM avant elle. « branch » accepte désormais une chaîne pour tout le parc ou une carte {nom: branche}, et la commande distante est bâtie par machine dès que les branches diffèrent. Le terrain était prêt : launch_installs sait déjà prendre une commande par VM. Une VM peut donc porter develop pendant qu'une autre valide 1.6.0, sur le même déploiement et dans le même tableau de suivi. Un piège : la branche d'une rangée retombait sur develop au remontage, la liste étant rebâtie depuis le formulaire plutôt que depuis la surcharge qu'elle portait déjà. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 35 +++++++++++++++++++++++++++++---- script/todo/todo.py | 24 +++++++++++++++++----- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index e982af9..1f1f83e 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -166,6 +166,9 @@ def apply_profile( # spec entière depuis qu'il se choisit machine par machine ; # `desktop` n'est plus que le défaut commun. "desktop": desktop, + # Branche ERPLibre. Même raison : « » signifie « celle du + # formulaire », et une surcharge la remplace pour cette VM. + "branch": "", } ) return out @@ -861,6 +864,11 @@ def run_deploy_form(ctx, run_app: bool = True): def _mise_usable(self): return any(vm["arch"] in mise_arches for vm in self.vms) + def _branch(self): + """Branche du formulaire : le défaut de chaque VM.""" + value = self.query_one("#f_branch", Select).value + return value if isinstance(value, str) else branches[0] + def _default_desktop(self): """« » pour un serveur, sinon la clé de la saveur choisie.""" """Type de VM par défaut. Chaque rangée peut s'en écarter.""" @@ -983,11 +991,17 @@ def run_deploy_form(ctx, run_app: bool = True): Select( [(b, b) for b in branches], classes="vmbranch", + # Repli sur la branche du FORMULAIRE, jamais sur + # branches[0] : les rangées sont remontées dès que le + # jeu de VM change (une entrée cochée, une copie + # ajoutée, un renommage), et elles retombaient alors + # toutes sur « develop » quel que soit le choix commun. value=( - item.get("branch") - or self.rows[i]["vm"].get("branch") - or branches[0] - ), + self.rows[i]["vm"].get("branch") + if i < len(self.rows) + else "" + ) + or self._branch(), allow_blank=False, id=f"v{i}_branch", ), @@ -1322,6 +1336,19 @@ def run_deploy_form(ctx, run_app: bool = True): # suivra donc le profil s'il change — ce qui est aussi le plus # attendu quand on n'a rien changé de visible. vm_now = self.rows[index]["vm"] + if field == "branch": + # « la branche du formulaire » n'est pas une surcharge : + # la VM doit suivre si on la change en haut. + current = vm_now.get("branch") or self._branch() + if event.value == current: + return + self._set_override( + index, + "branch", + "" if event.value == self._branch() else event.value, + ) + self._recompute() + return if field == "type": new_desk = "" if event.value == SERVER else event.value if new_desk == (vm_now.get("desktop") or ""): diff --git a/script/todo/todo.py b/script/todo/todo.py index db3253e..c706508 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4898,8 +4898,12 @@ class TODO: # commande distante en dépend, donc elle se construit par VM ; celle-ci # reste le défaut pour les noms absents du dict. desk_map = desktop if isinstance(desktop, dict) else {} + # Même contrat que `desktop` : une chaîne pour tout le parc, ou + # une carte {nom: branche} quand elles diffèrent d'une VM à l'autre. + branch_map = branch if isinstance(branch, dict) else {} + branch_def = "" if branch_map else branch remote = self._qemu_erplibre_remote_cmd( - branch, + branch_def, final_cmd, prod, "" if desk_map else desktop, @@ -4928,9 +4932,9 @@ class TODO: "version": v, "arch": a, } - if desk_map: + if desk_map or branch_map: entry["remote_cmd"] = self._qemu_erplibre_remote_cmd( - branch, + branch_map.get(name, branch_def), final_cmd, prod, desk_map.get(name, ""), @@ -4943,7 +4947,9 @@ class TODO: if not vms: print(t("No VM to install.")) return - manifest = launch_installs(vms, branch, remote) + manifest = launch_installs( + vms, branch_def or next(iter(branch_map.values()), ""), remote + ) print(f"\n🖥 {t('Opening the interactive monitor...')}") # Affiche tous les chemins de log (pour les consulter/partager même si # on quitte le dashboard avant la fin). @@ -6291,6 +6297,14 @@ class TODO: desktop = next((d for d in desktop_map.values() if d), "") python_provider = spec.get("python_provider") or "" app_store = spec.get("app_store") or "deb" + # Branche par VM : « » sur une VM veut dire « celle du formulaire ». + branch_map = { + vm["name"]: (vm.get("branch") or install_branch or "") + for vm in pending + } + for _n in deployed: + branch_map.setdefault(_n, install_branch or "") + branch_multi = len(set(branch_map.values())) > 1 ssh_key = spec.get("ssh_key") add_ssh_config = spec["add_ssh_config"] parallelism = spec["parallelism"] @@ -6360,7 +6374,7 @@ class TODO: # Installs détachées en parallèle + dashboard Textual. self._qemu_install_erplibre_monitored( deployed, - install_branch, + branch_map if branch_multi else install_branch, ip_map, install["cmd"] if install else None, install["prod"] if install else False, From 8a946a0b89b760dcfd49699ab478be4fa0fa316a Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 01:15:13 -0400 Subject: [PATCH 33/41] [ADD] tui qemu: an Odoo version per VM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A profile list per row, beside the branch: "ERPLibre + Odoo 18", "Odoo 17", "ERPLibre alone". The form's profile stays the default; picking another for a VM creates an override, going back clears it. It reaches down to execution, like the branch and the type before it. "final_cmd" now takes either a string for the whole fleet or a {name: command} map, and the remote command is built per machine as soon as profiles differ. A VM can install Odoo 18 while another validates Odoo 17, in the same deployment. Both lists are widened -- branch to 24 columns, profile to 28. Truncated, "develop" and "ERPLibre + Odoo 18" no longer showed what had been picked, and that is precisely what one wants to re-read before deploying. Two more defects: the global branch and Odoo choices reached nothing at all, and the summary announced the global profile for every VM, then named a version nothing would install. --- FR --- Une liste de profils par rangée, à côté de la branche : « ERPLibre + Odoo 18 », « Odoo 17 », « ERPLibre seul ». Le profil du formulaire reste le défaut ; en choisir un autre pour une VM crée une surcharge, y revenir l'efface. Il descend jusqu'à l'exécution, comme la branche et le type avant lui. « final_cmd » accepte maintenant une chaîne pour tout le parc ou une carte {nom: commande}, et la commande distante est bâtie par machine dès que les profils diffèrent. Une VM peut donc installer Odoo 18 pendant qu'une autre valide Odoo 17, dans le même déploiement. Les deux listes sont élargies : la branche passe à 24 colonnes, le profil à 28. Tronqués, « develop » et « ERPLibre + Odoo 18 » ne laissaient plus voir ce qu'on avait choisi — et c'est précisément ce qu'on veut relire avant de déployer. Deux autres défauts : les choix globaux de branche et d'Odoo n'atteignaient rien, et le sommaire annonçait le profil global pour toutes les VM, puis nommait une version que rien n'installait. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 81 +++++++++++++++++++++++++++++++-- script/todo/todo.py | 59 +++++++++++++++++++++--- 2 files changed, 131 insertions(+), 9 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 1f1f83e..04a787d 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -169,6 +169,12 @@ def apply_profile( # Branche ERPLibre. Même raison : « » signifie « celle du # formulaire », et une surcharge la remplace pour cette VM. "branch": "", + # Profil d'installation (« ERPLibre + Odoo 18 »). Même + # convention : « » = celui du formulaire. + "install_cmd": "", + # Libelle du profil, pour que le recapitulatif puisse dire + # « Odoo 18 » sans connaitre la liste des profils. + "install_label": "", } ) return out @@ -449,7 +455,10 @@ def run_deploy_form(ctx, run_app: bool = True): #body { height: 1fr; } #fields { width: 62; border: solid $accent; overflow-y: auto; } #right { width: 1fr; } - #plan { height: 1fr; border: solid $accent; } + #plan { + height: 1fr; border: solid $accent; + overflow-x: auto; scrollbar-size-horizontal: 1; + } #totals { height: auto; color: $text-muted; padding: 0 1; } .grouptitle { color: $accent; text-style: bold; padding: 1 0 0 0; } SelectionList { height: 10; border: solid $panel; } @@ -473,12 +482,22 @@ def run_deploy_form(ctx, run_app: bool = True): /* La branche porte des noms longs (« 1.6.0 », « develop », « feature/xyz ») : trop étroite, la liste les tronque et on ne sait plus ce qu'on a choisi. */ - .vmbranch { width: 18; } + + .vmcopy { width: 5; min-width: 5; } .vmhead { height: 1; } - .vmrow { height: 3; align-vertical: middle; } + /* « width: auto » et le défilement du plan : sans eux, une rangée + plus large que le panneau est COUPÉE au lieu d'être atteignable. */ + .vmrow { height: 3; width: auto; align-vertical: middle; } .vmrow Select { width: 15; } .vmrow Input { width: 11; } + /* Ces deux règles portent « .vmrow Select » EN PLUS de leur classe : + « .vmrow Select » (une classe + un type) l'emporte sur « .vmbranch » + (une classe) par spécificité CSS. Écrites simplement, elles étaient + silencieusement écrasées à 15 — et le test, qui ne vérifiait que la + présence de la classe, passait sans rien prouver. */ + .vmrow Select.vmbranch { width: 34; } + .vmrow Select.vmprof { width: 40; } #reslabel { color: $text-muted; } RenameScreen { align: center middle; } #renbox { @@ -864,6 +883,24 @@ def run_deploy_form(ctx, run_app: bool = True): def _mise_usable(self): return any(vm["arch"] in mise_arches for vm in self.vms) + def _profile_cmd(self): + """Commande du profil choisi en haut : le défaut de chaque VM.""" + if not profiles: + return "" + index = self.query_one("#f_profile_install", Select).value + return profiles[index if isinstance(index, int) else 0][1] + + def _row_profile_index(self, i): + """Rang du profil que la rangée doit AFFICHER.""" + cmd = "" + if i < len(self.rows): + cmd = self.rows[i]["vm"].get("install_cmd") or "" + cmd = cmd or self._profile_cmd() + for k, (_lbl, c) in enumerate(profiles): + if c == cmd: + return k + return 0 + def _branch(self): """Branche du formulaire : le défaut de chaque VM.""" value = self.query_one("#f_branch", Select).value @@ -1005,6 +1042,17 @@ def run_deploy_form(ctx, run_app: bool = True): allow_blank=False, id=f"v{i}_branch", ), + ( + Select( + [(lbl, i) for i, (lbl, _c) in enumerate(profiles)], + value=self._row_profile_index(i), + allow_blank=False, + classes="vmprof", + id=f"v{i}_prof", + ) + if profiles + else Static("", classes="vmprof") + ), Select( self._type_options(), value=vm.get("desktop") or SERVER, @@ -1093,6 +1141,7 @@ def run_deploy_form(ctx, run_app: bool = True): for wid, value in ( (f"#v{i}_type", vm.get("desktop") or SERVER), (f"#v{i}_branch", vm.get("branch") or self._branch()), + (f"#v{i}_prof", self._row_profile_index(i)), ): try: self.query_one(wid, Select).value = value @@ -1262,6 +1311,9 @@ def run_deploy_form(ctx, run_app: bool = True): continue for field in fields: changed |= self.overrides[key].pop(field, None) is not None + if field == "install_cmd": + # Le libelle n'a pas de sens sans sa commande. + self.overrides[key].pop("install_label", None) if not self.overrides[key]: self.overrides.pop(key, None) if changed: @@ -1336,6 +1388,21 @@ def run_deploy_form(ctx, run_app: bool = True): # suivra donc le profil s'il change — ce qui est aussi le plus # attendu quand on n'a rien changé de visible. vm_now = self.rows[index]["vm"] + if field == "prof": + label, cmd = profiles[event.value] + if cmd == ( + vm_now.get("install_cmd") or self._profile_cmd() + ): + return + same = cmd == self._profile_cmd() + self._set_override( + index, "install_cmd", "" if same else cmd + ) + self._set_override( + index, "install_label", "" if same else label + ) + self._recompute() + return if field == "branch": # « la branche du formulaire » n'est pas une surcharge : # la VM doit suivre si on la change en haut. @@ -1472,6 +1539,14 @@ def run_deploy_form(ctx, run_app: bool = True): "install_cmd": ( vm.get("install_cmd") or self._profile_cmd() ), + "install_label": ( + vm.get("install_label") + or ( + profiles[self._row_profile_index(index)][0] + if profiles + else "" + ) + ), } else: self.locked.discard(key) diff --git a/script/todo/todo.py b/script/todo/todo.py index c706508..7e6528a 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4902,9 +4902,13 @@ class TODO: # une carte {nom: branche} quand elles diffèrent d'une VM à l'autre. branch_map = branch if isinstance(branch, dict) else {} branch_def = "" if branch_map else branch + # Idem pour le profil : « ERPLibre + Odoo 18 » peut differer d'une + # machine a l'autre, on valide alors deux versions d'un coup. + cmd_map = final_cmd if isinstance(final_cmd, dict) else {} + cmd_def = None if cmd_map else final_cmd remote = self._qemu_erplibre_remote_cmd( branch_def, - final_cmd, + cmd_def, prod, "" if desk_map else desktop, python_provider, @@ -4932,10 +4936,10 @@ class TODO: "version": v, "arch": a, } - if desk_map or branch_map: + if desk_map or branch_map or cmd_map: entry["remote_cmd"] = self._qemu_erplibre_remote_cmd( branch_map.get(name, branch_def), - final_cmd, + cmd_map.get(name, cmd_def), prod, desk_map.get(name, ""), python_provider, @@ -5361,10 +5365,33 @@ class TODO: gigs = self._parse_disk_gb(vm["disk"]) + ( self.ERPLIBRE_EXTRA_DISK_GB if branch else 0 ) + # Ce qui S'ECARTE du choix commun se dit sur la ligne de la VM. + # Sans cela le sommaire annoncait le profil general pour tout le + # monde, y compris pour une VM figee sur un autre — on lisait + # « Odoo 15 » avant de deployer une machine en Odoo 18. + apart = [] + # Seul ce qui DIFFERE vaut d'etre signale : une VM figee sur la + # meme branche que le global n'a rien de particulier a montrer, + # et repeter la valeur commune sur chaque ligne la noierait. + if install and vm.get("branch") and vm["branch"] != branch: + apart.append(vm["branch"]) + if ( + vm.get("install_label") + and install + and vm.get("install_cmd") != install.get("cmd") + ): + apart.append(vm["install_label"]) + if vm.get("desktop") and vm["desktop"] != spec.get("desktop"): + apart.append( + (self._QEMU_DESKTOP.get(vm["desktop"]) or {}).get( + "label", vm["desktop"] + ) + ) print( f" {vm['name']:<30} {vm['distro']} {vm['version']:<7} " f"[{vm['arch']:<5}] {vm['vcpus']} vCPU RAM {vm['ram']}Mo " f"{t('disk')} {gigs}G" + + (f" ⟵ {' · '.join(apart)}" if apart else "") ) if existing: print(f" {t('Existing, left untouched:')} {', '.join(existing)}") @@ -5374,9 +5401,22 @@ class TODO: if install["prod"] else t("development (~/git)") ) + # « par defaut » : chaque VM peut s'en ecarter, et sa ligne le dit. + # Ce qui sera REELLEMENT pose, pas le defaut du formulaire. Avec + # une seule VM figee sur un autre profil, annoncer le defaut + # revenait a nommer une version que rien n'installe — c'est + # exactement ce qu'on relit ici pour eviter de se tromper. + used_br = {vm.get("branch") or branch for vm in spec["vms"]} + used_lb = { + vm.get("install_label") or install["label"] + for vm in spec["vms"] + } + varies = t("varies, see each line") + br_txt = used_br.pop() if len(used_br) == 1 else varies + lb_txt = used_lb.pop() if len(used_lb) == 1 else varies print( - f" {t('ERPLibre install:')} {t('branch')} {branch}, " - f"{t('profile')} {install['label']}, {env}" + f" {t('ERPLibre install:')} {t('branch')} {br_txt}, " + f"{t('profile')} {lb_txt}, {env}" ) else: print(f" {t('ERPLibre install:')} {t('no')}") @@ -6305,6 +6345,13 @@ class TODO: for _n in deployed: branch_map.setdefault(_n, install_branch or "") branch_multi = len(set(branch_map.values())) > 1 + base_cmd = install["cmd"] if install else None + cmd_map = { + vm["name"]: (vm.get("install_cmd") or base_cmd) for vm in pending + } + for _n in deployed: + cmd_map.setdefault(_n, base_cmd) + cmd_multi = len(set(cmd_map.values())) > 1 ssh_key = spec.get("ssh_key") add_ssh_config = spec["add_ssh_config"] parallelism = spec["parallelism"] @@ -6376,7 +6423,7 @@ class TODO: deployed, branch_map if branch_multi else install_branch, ip_map, - install["cmd"] if install else None, + cmd_map if cmd_multi else base_cmd, install["prod"] if install else False, desktop=desktop_map, python_provider=python_provider, From c6c860254ca0881faa92c92b69d9ae8d210fe92b Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 01:56:36 -0400 Subject: [PATCH 34/41] [ADD] tui qemu: pick the time zone from a list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The timezone was typed by hand. A misspelt IANA name is not rejected by cloud-init: it is IGNORED. The VM stays on UTC, and you only notice from the timestamps, once deployed. A list of twenty-five zones, Québec first, then the rest of Canada and the places one actually meets. The host's zone goes to the top, without duplication: a machine outside this list must still see its own at a glance. NAMES, not offsets: "UTC-5" says nothing about daylight saving and cloud-init will not take it. A name carries its own switching rules. "free value…" keeps the door open to the other six hundred zones in the database. The choice is copied into the field, which stays the only value the spec reads -- one place holds the answer. --- FR --- Le fuseau se tapait à la main. Un nom IANA mal orthographié n'est pas refusé par cloud-init : il est IGNORÉ. La VM reste en UTC, et on ne s'en aperçoit qu'aux horodatages, une fois déployée. Une liste de vingt-cinq fuseaux, le Québec d'abord, puis le reste du Canada et les places qu'on rencontre en pratique. Le fuseau de l'hôte passe en tête, sans doublon : une machine hors de cette liste doit voir le sien en un coup d'œil. Des NOMS, pas des décalages : « UTC-5 » ne dit rien de l'heure d'été et cloud-init n'en veut pas. Un nom porte ses propres règles de bascule. « libre… » garde la porte ouverte aux six cents autres fuseaux de la base. Le choix est recopié dans le champ, qui reste la seule valeur lue par la spec — un seul endroit porte la réponse. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 28 +++++++++++++++++++ script/todo/todo.py | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 04a787d..61c5397 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -370,6 +370,8 @@ def run_deploy_form(ctx, run_app: bool = True): # livrent snapd — la question n'a de sens que pour celles-là, graphiques. app_stores = list(ctx.get("app_stores") or []) snap_distros = set(ctx.get("snap_distros") or ()) + # Fuseaux proposés, celui de l'hôte en tête (voir todo.py). + timezones = list(ctx.get("timezones") or []) defaults = ctx.get("defaults") or {} result = {"spec": None} @@ -683,10 +685,23 @@ def run_deploy_form(ctx, run_app: bool = True): id="f_monitor", ) yield Static(t("Timezone"), classes="grouptitle") + # Une liste plutôt qu'une saisie : un nom IANA mal + # orthographié n'est pas refusé par cloud-init, il est + # IGNORÉ — la VM reste en UTC et on ne s'en aperçoit + # qu'aux horodatages. « libre… » garde la porte ouverte + # aux six cents autres fuseaux de la base. + yield Select( + [(z, z) for z in timezones] + + [(t("free value…"), FREE)], + value=(timezones[0] if timezones else SELECT_NULL), + allow_blank=False, + id="f_tz_sel", + ) yield Input( value=ctx.get("timezone") or "", placeholder=t("Timezone for the VMs"), id="f_tz", + classes="freeval", ) yield Static("SSH", classes="grouptitle") yield Input( @@ -1452,6 +1467,19 @@ def run_deploy_form(ctx, run_app: bool = True): # ci-dessous sans rien recalculer, et les rangées restaient sur # l'ancienne version. Elles n'en gardent pas de copie — « » y # veut dire « celle du formulaire » — il suffit de redessiner. + if event.select.id == "f_tz_sel": + # « libre… » révèle la saisie ; un fuseau choisi la referme et + # y recopie le nom, seule valeur que lisent _form_values et la + # spec — un seul endroit porte la réponse. + free = event.value is FREE + field = self.query_one("#f_tz", Input) + field.display = free + field.disabled = not free + if free: + field.focus() + elif isinstance(event.value, str): + field.value = event.value + return if event.select.id in ("f_branch", "f_profile_install"): self._clear_overrides( ("branch",) diff --git a/script/todo/todo.py b/script/todo/todo.py index 7e6528a..949ab41 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4413,6 +4413,52 @@ class TODO: ) QEMU_SNAP_DISTROS = ("ubuntu",) + # Fuseaux proposés au déploiement. Des NOMS IANA, pas des décalages : + # cloud-init écrit /etc/timezone et refuse « UTC-5 », qui ne dit d'ailleurs + # rien de l'heure d'été. Un nom porte ses propres règles de bascule. + # + # Liste courte et ordonnée par usage réel plutôt qu'exhaustive : la base + # IANA en compte près de six cents, illisibles dans une liste déroulante. + # Le Québec d'abord, le reste du Canada ensuite, puis les places qu'on + # rencontre en pratique. La saisie libre reste offerte pour le reste. + QEMU_TIMEZONES = ( + "America/Montreal", + "America/Toronto", + "America/Halifax", + "America/Winnipeg", + "America/Edmonton", + "America/Vancouver", + "America/St_Johns", + "UTC", + "America/New_York", + "America/Chicago", + "America/Denver", + "America/Los_Angeles", + "America/Sao_Paulo", + "Europe/London", + "Europe/Paris", + "Europe/Brussels", + "Europe/Zurich", + "Europe/Madrid", + "Europe/Berlin", + "Africa/Casablanca", + "Asia/Dubai", + "Asia/Kolkata", + "Asia/Shanghai", + "Asia/Tokyo", + "Australia/Sydney", + ) + + @classmethod + def _qemu_timezone_choices(cls, current=""): + """Liste à proposer : le fuseau de l'hôte en tête, sans doublon. + + Le mettre en premier plutôt que de le supposer présent : une machine + hors de cette liste doit quand même voir le sien en un coup d'œil.""" + out = [current] if current else [] + out += [z for z in cls.QEMU_TIMEZONES if z != current] + return out + @classmethod def _qemu_desktop_suffixes(cls): """{clé de saveur: suffixe de nom}. La TUI le reçoit par son contexte @@ -5762,6 +5808,9 @@ class TODO: "desktop_disk_gb": self.QEMU_DESKTOP_EXTRA_DISK_GB, "mise_arches": self.QEMU_MISE_ARCHES, "app_stores": [(k, t(lbl)) for k, lbl in self.QEMU_APP_STORES], + "timezones": self._qemu_timezone_choices( + self._qemu_host_timezone() + ), "snap_distros": self.QEMU_SNAP_DISTROS, "desktop_suffixes": self._qemu_desktop_suffixes(), "desktops": [ From a3aac909ddaa2b10bac0f7cbf931d678efe902f1 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 03:04:02 -0400 Subject: [PATCH 35/41] [ADD] qemu desktop: say how to REACH the remote desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VM announced "Remote desktop: RDP 3389" without saying how to get there. Its address is on libvirt's private network, which is not routed from outside its host: the port alone helps nobody. It now prints the tunnel to set up, address already filled in -- "ssh -L 3390::3389 @", then an RDP client on localhost:3390. Likewise in VNC for Arch, on 5902. This is the only workable route when the libvirt host is itself a VM with no graphical interface: the SPICE console is on "listen=none" and assumes virt-viewer ON that host. --- FR --- La VM annonçait « Bureau distant : RDP 3389 » sans dire par où passer. Son adresse est sur le réseau privé de libvirt, qui n'est pas routé depuis l'extérieur de son hôte : le port seul n'aide personne. Elle imprime maintenant le tunnel à monter, adresse déjà remplie — « ssh -L 3390::3389 @ », puis un client RDP sur localhost:3390. Idem en VNC pour Arch, sur 5902. C'est le seul chemin praticable quand l'hôte libvirt est lui-même une VM sans interface graphique : la console SPICE est en « listen=none » et suppose virt-viewer SUR cet hôte. Assisted-by: Claude Opus 5 --- script/todo/todo.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/script/todo/todo.py b/script/todo/todo.py index 949ab41..50f0202 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4664,6 +4664,22 @@ class TODO: f'echo " {t("openSUSE mirror:")} $zm"; break; fi; done; ' ) + @staticmethod + def _qemu_tunnel_hint(port, kind): + """Deux lignes imprimees DANS la VM : le tunnel a monter depuis le + poste de travail, avec l'adresse deja remplie. + + Un port annonce sans chemin pour y arriver n'aide personne : le reseau + libvirt n'est pas route depuis l'exterieur de son hote.""" + local = port + 1 + return ( + "ip=$(hostname -I 2>/dev/null | awk '{print $1}'); " + f'echo " {t("From your workstation:")} ' + f'ssh -L {local}:$ip:{port} @"; ' + f'echo " {t("then point your client at")} ' + f'localhost:{local} ({kind})"; ' + ) + def _qemu_desktop_remote_cmd(self, flavour="gnome", app_store="deb"): """Bloc shell installant le bureau choisi + son accès distant, quelle que soit la distribution. Même aiguillage que l'installation ERPLibre, @@ -4716,9 +4732,18 @@ class TODO: "if command -v xrdp >/dev/null 2>&1; then " "sudo systemctl enable --now xrdp >/dev/null 2>&1 || true; " f'echo " {t("Remote desktop:")} RDP 3389"; ' - "elif command -v vncserver >/dev/null 2>&1; then " + # L'IP est sur le reseau PRIVE de libvirt : annoncer le port sans + # dire comment l'atteindre ne sert a rien. La VM connait sa propre + # adresse ; seul le nom de l'hote libvirt manque, et c'est le + # lecteur qui l'a. La console SPICE, elle, est en « listen=none » + # et suppose virt-viewer SUR l'hote — inutilisable quand cet hote + # est lui-meme une VM sans interface graphique. + + self._qemu_tunnel_hint(3389, "RDP") + + "elif command -v vncserver >/dev/null 2>&1; then " f'echo " {t("Remote desktop:")} VNC 5901 ' - '(vncpasswd puis vncserver :1)"; fi; ' + '(vncpasswd puis vncserver :1)"; ' + + self._qemu_tunnel_hint(5901, "VNC") + + "fi; " ) # mise ne publie de binaire que pour ces architectures : 46 assets à la From 75498384c95bbddb715284e95a0d6d657ee8e83f Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 03:40:22 -0400 Subject: [PATCH 36/41] [ADD] qemu: build the tunnel to the remote desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A "Remote desktop tunnel" entry under SSH configuration. It lists the targets, resolves them, and composes the full command -- nothing to fill in. todo.py runs on the libvirt HOST; the tunnel starts from the workstation. It cannot open it, but it alone knows the VM's private IP, its port and the address through which it was reached. That last one comes from SSH_CONNECTION, whose third field is exactly the server address used -- far safer than a "hostname" that may resolve to nothing outside. When ~/.ssh/config already carries the entry, the tunnel takes it: "ssh -N -L 5902:localhost:5901 ". ProxyJump makes the route and "localhost" means the VM's OWN loopback, so the tunnel survives an IP change. That config is also the right source of targets, not the local libvirt: a graphical VM is often nested, and virsh would only ever list the orchestrator. Two details: the hypervisor console needs VNC bound to the loopback, as "listen=none" opens no socket at all; and two menu entries had no icon. --- FR --- Une entrée « Tunnel bureau distant » sous Configuration SSH. Elle liste les cibles, les résout, et compose la commande complète — rien à remplir. todo.py tourne sur l'HÔTE libvirt ; le tunnel, lui, part du poste de travail. Il ne peut donc pas l'ouvrir, mais il est le seul à connaître l'IP privée de la VM, son port et l'adresse par laquelle on l'a joint. Cette dernière vient de SSH_CONNECTION, dont le troisième champ est exactement l'adresse serveur utilisée — bien plus sûr qu'un « hostname » qui peut ne rien résoudre depuis l'extérieur. Quand ~/.ssh/config porte déjà l'entrée, le tunnel l'emprunte : « ssh -N -L 5902:localhost:5901 ». Le ProxyJump fait la route et « localhost » désigne le bouclage DE LA VM, si bien que le tunnel survit à un changement d'IP. Ce fichier est aussi la bonne source de cibles, pas le libvirt local : une VM graphique est souvent imbriquée, et virsh ne listerait jamais que l'orchestrateur. Deux détails : la console de l'hyperviseur exige un VNC sur la boucle locale, « listen=none » n'ouvrant aucun socket ; et deux entrées de menu n'avaient pas d'icône. Assisted-by: Claude Opus 5 --- script/qemu/deploy_qemu.py | 17 +++- script/todo/todo.py | 160 +++++++++++++++++++++++++++++++++++++ script/todo/todo_i18n.py | 8 +- 3 files changed, 180 insertions(+), 5 deletions(-) diff --git a/script/qemu/deploy_qemu.py b/script/qemu/deploy_qemu.py index 66e596d..4f483de 100755 --- a/script/qemu/deploy_qemu.py +++ b/script/qemu/deploy_qemu.py @@ -1787,7 +1787,22 @@ def virt_install( "\n Le bureau sera accessible à distance, par le réseau." ) else: - graphics = "spice,listen=none" + # VNC sur la boucle locale, PAS « spice,listen=none ». + # + # « listen=none » est le défaut de virt-install et il n'expose + # rien : QEMU crée l'affichage mais n'ouvre AUCUN socket TCP, seul + # le canal libvirt y mène. Un virt-manager tournant sur la machine + # même y accède ; rien d'autre. Or l'hôte QEMU est lui-même une VM + # ici — la console était donc inatteignable par construction, et + # aucun « ssh -L » ne pouvait y remédier : il n'y avait pas de port + # où aboutir. + # + # 127.0.0.1 n'expose rien au réseau non plus : le port n'est + # joignable que depuis l'hôte, donc à travers un tunnel SSH. VNC + # plutôt que SPICE parce qu'il tient en UN port — un seul « -L » + # suffit, avec n'importe quel client. Pour revenir au comportement + # d'avant : --graphics spice,listen=none + graphics = "vnc,listen=127.0.0.1" video = ["--video", "virtio"] cmd = [ "virt-install", diff --git a/script/todo/todo.py b/script/todo/todo.py index 50f0202..e1fe8d3 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -11,6 +11,7 @@ import inspect import json import logging import os +import socket import re import shlex import shutil @@ -1282,6 +1283,11 @@ class TODO: "SSH configuration (~/.ssh/config, ProxyJump)" ) }, + { + "prompt_description": t( + "Remote desktop tunnel (VNC/RDP through SSH)" + ) + }, {"section": t("Catalog")}, {"prompt_description": t("List available images and specs")}, ] @@ -1322,6 +1328,8 @@ class TODO: elif status == "13": self._qemu_ssh_config_menu() elif status == "14": + self._qemu_tunnel_menu() + elif status == "15": self._qemu_list_images() else: cmd_no_found = True @@ -1473,6 +1481,158 @@ class TODO: roots.append({"alias": name, "ip": ip}) return roots + # Ports du bureau distant, par gestionnaire de paquets de la VM. Ils + # viennent de _QEMU_DESKTOP_REMOTE, seule source : xrdp sur 3389 partout, + # sauf Arch qui reçoit TigerVNC sur 5901. + @classmethod + def _qemu_desktop_port(cls, distro): + if distro == "arch": + return cls._QEMU_DESKTOP_REMOTE["pacman"]["port"], "VNC" + return cls._QEMU_DESKTOP_REMOTE["apt"]["port"], "RDP" + + @staticmethod + def _qemu_self_address(): + """Adresse par laquelle l'utilisateur a JOINT cet hôte. + + SSH_CONNECTION porte « ip_client port_client ip_serveur port_serveur » : + le troisième champ est exactement l'adresse à remettre dans la commande + de tunnel, bien mieux qu'un « hostname » qui peut ne rien résoudre + depuis le poste de travail. Hors session SSH, on retombe sur le nom + d'hôte, en le signalant.""" + conn = os.environ.get("SSH_CONNECTION", "").split() + if len(conn) >= 3: + return conn[2], True + return socket.gethostname(), False + + def _qemu_tunnel_menu(self): + """Commande de tunnel SSH vers le bureau distant d'une machine. + + La source des cibles est ~/.ssh/config, PAS le libvirt local. La VM + graphique est souvent imbriquee : un orchestrateur QEMU tourne dans une + VM, et la machine a bureau vit DANS cet orchestrateur. Le « virsh » du + poste ne voit alors que l'orchestrateur, et proposer sa liste menait + droit a la mauvaise machine — vecu. + + ~/.ssh/config, lui, connait les deux, ProxyJump compris : c'est la + seule vue qui traverse les niveaux. Les domaines libvirt LOCAUX sont + ajoutes en complement quand ils ne s'y trouvent pas deja. + """ + print(f"\n🖥 {t('Remote desktop tunnel')}") + hosts = list(self._ssh_config_hosts()) + targets = [(h, "ssh_config") for h in hosts] + # Complement local, sans sudo tant qu'on n'en a pas besoin : la + # plupart des cibles utiles sont deja dans ssh_config. + if not targets: + for name in self._qemu_list_domains(): + targets.append((name, "virsh")) + if not targets: + print(f" {t('No host in ~/.ssh/config and no local VM.')}") + return + for i, (name, src) in enumerate(targets, 1): + mark = "" if src == "ssh_config" else f" ({t('local VM')})" + print(f" [{i}] {name}{mark}") + answer = input(f"{t('Which VM?')} [1]: ").strip() or "1" + if not answer.isdigit() or not (1 <= int(answer) <= len(targets)): + print(t("Cancelled.")) + return + name, src = targets[int(answer) - 1] + + # Le port ne se devine pas pour un hote de ssh_config : on ne connait + # ni sa distribution ni son bureau. On propose, l'utilisateur tranche. + print(f"\n {t('Remote desktop kind:')}") + print(f" [1] RDP 3389 (xrdp) *") + print(f" [2] VNC 5901 (TigerVNC, Arch)") + print(f" [3] {t('Hypervisor console (QEMU screen, no guest server)')}") + kind_answer = input(f"{t('Choice')} [1]: ").strip() or "1" + if kind_answer == "3": + self._qemu_console_tunnel() + return + port, kind = (5901, "VNC") if kind_answer == "2" else (3389, "RDP") + local = port + 1 + + print(f"\n {t('Run this on YOUR workstation:')}") + if src == "ssh_config": + # « localhost » est resolu par le DERNIER saut, donc par la machine + # elle-meme : le ProxyJump de ssh_config traverse les niveaux. + print(f"\n ssh -N -L {local}:localhost:{port} {name}\n") + print(f" {t('(through the ProxyJump already in ~/.ssh/config)')}") + else: + ip = self._qemu_resolve_ips([name]).get(name) + if not ip: + print(f" {t('No IP for this VM; is it running?')}") + return + host, from_ssh = self._qemu_self_address() + user = os.environ.get("USER", "user") + if not from_ssh: + print( + f" ⚠ {t('Not in an SSH session: check the host address.')}" + ) + print(f"\n ssh -N -L {local}:{ip}:{port} {user}@{host}\n") + print(f" ⚠ {t('No ~/.ssh/config entry; see SSH configuration.')}") + print( + f" {t('then point your client at')} localhost:{local} ({kind})" + ) + print(f" {t('The tunnel stays open as long as that ssh runs.')}") + + def _qemu_console_tunnel(self): + """Tunnel vers l'ÉCRAN QEMU d'une VM, pas vers un serveur de l'invité. + + Les deux autres choix du menu supposent un service DANS l'invité — + xrdp, TigerVNC — donc une session de bureau déjà ouverte et un mot de + passe posé. La console de l'hyperviseur, elle, existe dès l'amorçage et + ne demande rien à l'invité : c'est ce que montre virt-manager. + + Le port n'est pas devinable : libvirt l'attribue au démarrage. On le + lit donc, et l'absence de port est un diagnostic à part entière — avec + « listen=none » QEMU n'ouvre AUCUN socket, et aucun tunnel n'y peut + rien tant que le domaine n'est pas redéfini. + """ + names = self._qemu_list_domains() + if not names: + print(f" {t('No local VM.')}") + return + for i, name in enumerate(names, 1): + print(f" [{i}] {name}") + raw = input(f"{t('Which VM?')} [1]: ").strip() or "1" + if not raw.isdigit() or not (1 <= int(raw) <= len(names)): + print(t("Cancelled.")) + return + name = names[int(raw) - 1] + try: + res = subprocess.run( + ["sudo", "virsh", "vncdisplay", name], + capture_output=True, + text=True, + timeout=15, + ) + except (OSError, subprocess.SubprocessError): + res = None + # « 127.0.0.1:0 » désigne le port 5900, « :1 » le 5901, etc. + port = None + if res and res.returncode == 0: + disp = res.stdout.strip().rsplit(":", 1) + if len(disp) == 2 and disp[1].isdigit(): + port = 5900 + int(disp[1]) + if not port: + print(f"\n ⚠ {t('This VM exposes no VNC port.')}") + print(f" {t('Its display is likely spice with listen=none:')}") + print(f" sudo virsh dumpxml {name} | grep -A2 '") + print(f" sudo virsh start {name}") + print(f"\n {t('New VMs get this by default; see deploy_qemu.')}") + return + host, from_ssh = self._qemu_self_address() + user = os.environ.get("USER", "user") + if not from_ssh: + print(f" ⚠ {t('Not in an SSH session: check the host address.')}") + print(f"\n {t('Run this on YOUR workstation:')}") + print(f"\n ssh -N -L {port}:127.0.0.1:{port} {user}@{host}\n") + print(f" {t('then point your VNC client at')} localhost:{port}") + print(f" {t('The tunnel stays open as long as that ssh runs.')}") + def _qemu_ssh_config_menu(self): """Écrit les entrées ~/.ssh/config du parc QEMU. diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index a716ed4..775f041 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -3060,8 +3060,8 @@ TRANSLATIONS = { "en": "select all", }, "Delete VM(s)": { - "fr": "🗑 Effacer une ou plusieurs VM", - "en": "🗑 Delete VM(s)", + "fr": "🗑 Effacer une ou plusieurs VM", + "en": "🗑 Delete VM(s)", }, "No VM found.": { "fr": "Aucune VM trouvée.", @@ -3688,8 +3688,8 @@ TRANSLATIONS = { "en": "Verify SHA256 after download? (y/N, default: no): ", }, "QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)": { - "fr": "QEMU - Exemple dry-run (demo-vm, Ubuntu 24.04)", - "en": "QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)", + "fr": "🧰 QEMU - Exemple dry-run (demo-vm, Ubuntu 24.04)", + "en": "🧰 QEMU - Sample dry-run (demo-vm, Ubuntu 24.04)", }, # QEMU - statistics screen "Statistics (installs, durations, VMs)": { From f7f5e800fe16f628a86978b57d325e6377927e17 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 04:45:16 -0400 Subject: [PATCH 37/41] [ADD] test: what the tooling imports must be declared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit textual, urwid and lxml were already in the tooling requirements; the earlier table read as if they were missing, when it only compared against the system python. psycopg2 is the one absent, and stays so: nothing under script/ imports it, the analysis tools query PostgreSQL through psql, and the Odoo venvs declare it where Odoo needs it. A test now answers this by reading the imports instead of the venv. It bites both ways: a removed declaration, and a new undeclared import. Also chmod +X -> +x in the Dockerfile: uppercase only adds the bit to directories, or to files that already carry one. On what COPY had just laid down in 644 it did nothing. --- FR --- [ADD] test : ce que l'outillage importe doit être déclaré textual, urwid et lxml y étaient déjà ; le tableau précédent laissait croire le contraire, alors qu'il comparait au python du système. psycopg2 est le seul absent, et le reste : rien sous script/ ne l'importe, les outils d'analyse passent par psql, et les venvs Odoo le déclarent là où Odoo en a besoin. Un test répond désormais à la question en lisant les imports plutôt que le venv. Il mord des deux côtés : déclaration retirée, et import non déclaré. Aussi chmod +X -> +x dans le Dockerfile : la majuscule n'ajoute le bit qu'aux répertoires, ou aux fichiers qui en portent déjà un. Sur ce que COPY venait de déposer en 644, elle ne faisait rien. Assisted-by: Claude Opus 5 --- docker/Dockerfile | 6 +- test/test_tooling_requirements.py | 155 ++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100755 test/test_tooling_requirements.py diff --git a/docker/Dockerfile b/docker/Dockerfile index a93708f..32f95f2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -86,7 +86,11 @@ ENV ODOO_RC /etc/odoo/odoo.conf COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py -RUN chmod +X /usr/local/bin/wait-for-psql.py +# « +X » en majuscule n'ajoute le bit que si l'un des trois est DÉJÀ posé, +# ou sur un répertoire. Sur le fichier que COPY vient de déposer en 644, il +# ne faisait rien : le script restait non exécutable dans l'image, malgré +# sa place dans /usr/local/bin. +RUN chmod +x /usr/local/bin/wait-for-psql.py # Set default user when running the container USER odoo diff --git a/test/test_tooling_requirements.py b/test/test_tooling_requirements.py new file mode 100755 index 0000000..0457472 --- /dev/null +++ b/test/test_tooling_requirements.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +"""Ce que l'outillage importe doit être déclaré, pas seulement installé. + +Un paquet peut vivre dans `.venv.erplibre` sans figurer nulle part : arrivé +par la dépendance d'un autre, ou posé à la main un jour. Rien ne le réclame, +donc rien ne le réinstalle — il disparaît au prochain venv neuf, et l'outil +qui s'en servait tombe chez le suivant, pas chez celui qui l'a écrit. + +Le fichier porte déjà ce raisonnement en commentaire pour lxml. Ce test le +rend vérifiable au lieu de le laisser à la vigilance. + +Portée : le venv d'OUTILS. Les venvs Odoo ont leurs propres requirements, et +psycopg2 y est déclaré parce qu'Odoo en a besoin — l'outillage, lui, parle à +PostgreSQL par psql en sous-processus. +""" + +import ast +import glob +import os +import re +import sys +import unittest + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +REQUIREMENTS = os.path.join(REPO, "requirement", "erplibre_require-ments.txt") + +PATTERNS = ( + "script/analyse/*.py", + "script/analyse/*/*.py", + "script/odoo/migration/*.py", + "script/todo/*.py", +) + +# Ce que l'on écrit pour importer n'est pas ce que l'on écrit pour installer. +# La table est explicite : un nouveau décalage doit être ajouté ici en +# connaissance de cause, plutôt que d'être toléré par une correspondance +# approximative qui laisserait passer une vraie absence. +PACKAGE_OF_MODULE = { + "dotenv": "python-dotenv", +} + +# Modules fournis par l'environnement, jamais par ce fichier. +PROVIDED_ELSEWHERE = { + # Le venv Odoo, pas celui des outils : ces scripts tournent sous + # `odoo-bin shell`, qui apporte son propre interpréteur. + "odoo", +} + + +def tooling_files(): + return sorted( + {p for m in PATTERNS for p in glob.glob(os.path.join(REPO, m))} + ) + + +def third_party_imports(): + """{module: {fichiers qui l'importent}}, hors stdlib et hors modules locaux.""" + files = tooling_files() + local = {os.path.basename(p)[:-3] for p in files} + found = {} + for path in files: + with open(path) as handle: + tree = ast.parse(handle.read()) + for node in ast.walk(tree): + names = [] + if isinstance(node, ast.Import): + names = [alias.name.split(".")[0] for alias in node.names] + elif ( + isinstance(node, ast.ImportFrom) + and node.module + and not node.level + ): + names = [node.module.split(".")[0]] + for name in names: + if ( + name in sys.stdlib_module_names + or name in local + or name in PROVIDED_ELSEWHERE + or name == "script" + ): + continue + found.setdefault(name, set()).add(os.path.relpath(path, REPO)) + return found + + +def declared(): + with open(REQUIREMENTS) as handle: + return handle.read() + + +class TestEveryImportIsDeclared(unittest.TestCase): + def test_the_scan_finds_something(self): + # Un test qui n'inspecte rien passe toujours. + self.assertGreater(len(tooling_files()), 20) + self.assertGreater(len(third_party_imports()), 5) + + def test_no_third_party_import_is_undeclared(self): + text = declared() + missing = [] + for module, users in sorted(third_party_imports().items()): + package = PACKAGE_OF_MODULE.get(module, module) + # En début de ligne : « lxml » ne doit pas être satisfait par + # une mention en commentaire ni par « python-lxml-stubs ». + if not re.search( + rf"^\s*{re.escape(package)}\b", text, re.M | re.I + ): + missing.append(f"{module} ({package}) <- {sorted(users)[0]}") + self.assertEqual(missing, []) + + def test_the_alias_table_has_no_dead_entry(self): + # Une correspondance qui ne sert plus masque le jour où le module + # revient sous un autre nom. + imported = set(third_party_imports()) + for module in PACKAGE_OF_MODULE: + with self.subTest(module=module): + self.assertIn(module, imported) + + def test_the_check_would_notice_a_removal(self): + # La preuve que la recherche mord : un paquet réellement déclaré, + # retiré du texte, doit être vu comme manquant. + # `assertRegex` cherche SANS re.M : « ^ » n'y vaut qu'au tout début + # de la chaîne, et la vérification ci-dessus, elle, lit ligne à ligne. + # Un motif compilé fait dire au test la même chose qu'au code. + motif = re.compile(r"^\s*lxml\b", re.M) + text = declared() + self.assertRegex(text, motif) + without = motif.sub("", text) + self.assertNotRegex(without, motif) + + +class TestPsycopg2StaysOutOfTheToolingVenv(unittest.TestCase): + """Décision prise, et la raison avec — sinon elle se reprend à l'aveugle. + + Les outils d'analyse interrogent PostgreSQL par `psql` en sous-processus : + un choix délibéré, qui porte la garantie de lecture seule côté serveur + (PGOPTIONS). Déclarer psycopg2 ici ferait compiler une extension C à + chaque installation neuve, sans que rien ne l'importe. + """ + + def test_nothing_in_the_tooling_imports_psycopg2(self): + self.assertNotIn("psycopg2", third_party_imports()) + + def test_the_analysis_tools_query_through_psql(self): + path = os.path.join(REPO, "script", "analyse", "lib_analyse.py") + with open(path) as handle: + source = handle.read() + self.assertIn('"psql"', source) + self.assertIn("default_transaction_read_only=on", source) + + +if __name__ == "__main__": + unittest.main() From aaf02c4ee69787d2d567318973edfee482eb07c8 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 04:51:27 -0400 Subject: [PATCH 38/41] [ADD] tui qemu: F9 dumps the form state, widgets and model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A per-VM setting appears not to be taken into account, and nothing on screen tells which link failed: the gap may be between the widget and the model, or between the model and the spec. A screenshot shows neither. F9 writes both side by side into ~/.erplibre/deploy-form-dump.txt: profile, shared values, overrides, locks, row generation, then VM by VM what the list DISPLAYS against what the model HOLDS, and finally the spec that would go to deployment. The file can be pasted into a message, unlike an image, and it answers the question on its own: if the list shows 16384 while the model says 1024, the defect is in the intake; if they agree and the created VM differs, it is downstream, in deploy_qemu. --- FR --- Un réglage par VM ne semble pas pris en compte, et rien dans ce que l'écran montre ne permet de trancher : l'écart peut être entre le widget et le modèle, ou entre le modèle et la spec. Une capture d'écran ne dit ni l'un ni l'autre. F9 écrit les deux côte à côte dans ~/.erplibre/deploy-form-dump.txt : profil, valeurs communes, surcharges, verrous, génération des rangées, puis VM par VM ce que la liste AFFICHE contre ce que le modèle CONTIENT, et enfin la spec qui partirait au déploiement. Le fichier se recopie dans un message, contrairement à une image, et il répond seul à la question : si la liste montre 16384 et que le modèle dit 1024, le défaut est dans la prise en compte ; s'ils s'accordent et que la VM créée diffère, il est en aval, dans deploy_qemu. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 56 +++++++++++++++++++++++++++++++++ script/todo/todo_i18n.py | 2 ++ 2 files changed, 58 insertions(+) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 61c5397..d86f5e7 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -524,6 +524,7 @@ def run_deploy_form(ctx, run_app: bool = True): ("f5", "deploy", t("Deploy")), ("f4", "clear_vm", t("Reset VM")), ("f3", "preview", t("Preview")), + ("f9", "dump_state", t("Diagnostic dump")), ("f6", "select_all", t("All")), ("f7", "select_main", t("Main versions")), ("f8", "select_none", t("None")), @@ -1755,6 +1756,61 @@ def run_deploy_form(ctx, run_app: bool = True): lines = [build(vm, spec, True) for vm in spec["vms"]] self.push_screen(PreviewScreen(lines or [t("Nothing selected.")])) + def action_dump_state(self) -> None: + """Écrit l'état COMPLET dans un fichier, widgets ET modèle. + + Une capture d'écran ne dit pas si l'écart vient de ce qu'on voit + ou de ce qui sera déployé. Ce vidage met les deux côte à côte, VM + par VM : si la liste affiche 16384 et que le modèle dit 1024, le + défaut est dans la prise en compte ; s'ils s'accordent et que la + VM déployée diffère, il est en aval.""" + path = os.path.expanduser("~/.erplibre/deploy-form-dump.txt") + os.makedirs(os.path.dirname(path), exist_ok=True) + out = [ + "=== formulaire de deploiement ===", + f"profil={self.profile} custom={self.custom}", + f"verrous={sorted(self.locked)}", + f"surcharges={self.overrides}", + f"generation={self._gen} jeu_monte={self._shown_ids}", + f"branche_globale={self._branch()}", + f"profil_odoo_global={self._profile_cmd()}", + "", + " # widget -> modele, VM par VM", + ] + for i, r in enumerate(self.rows): + vm = r["vm"] + shown = {} + for f in ("vcpus", "ram", "disk", "type", "branch", "prof"): + try: + shown[f] = self.query_one(f"#v{i}_{f}", Select).value + except Exception: + shown[f] = "-" + try: + free = self.query_one(f"#c{i}_{f}", Input) + if free.display: + shown[f] = f"libre:{free.value!r}" + except Exception: + pass + out.append(f" [{i}] {vm['name']} etat={r['state']}") + out.append(f" widgets = {shown}") + out.append( + f" modele = vcpus={vm['vcpus']} ram={vm['ram']} " + f"disk={vm['disk']} desktop={vm.get('desktop')!r} " + f"branch={vm.get('branch')!r} cmd={vm.get('install_cmd')!r}" + ) + spec = build_spec(self.vms, domains, self._form_values()) + out.append("") + out.append(" # spec qui partirait au deploiement") + for vm in spec["vms"]: + out.append( + f" {vm['name']}: ram={vm['ram']} vcpus={vm['vcpus']} " + f"disk={vm['disk']}" + ) + out.append(f" ignorees (existent deja) = {spec['existing']}") + with open(path, "w", encoding="utf-8") as fh: + fh.write("\n".join(out) + "\n") + self.notify(f"{t('State written to')} {path}") + def action_deploy(self) -> None: if not self.vms: self.notify(t("Nothing selected."), severity="warning") diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 775f041..b4e69f7 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2275,6 +2275,8 @@ TRANSLATIONS = { "en": "already defined, not counted", }, "RAM: 2048 or 8G": {"fr": "RAM : 2048 ou 8G", "en": "RAM: 2048 or 8G"}, + "Diagnostic dump": {"fr": "Diagnostic", "en": "Diagnostic dump"}, + "State written to": {"fr": "État écrit dans", "en": "State written to"}, "Reset VM": { "fr": "Réinit. VM", "en": "Reset VM", From f97e27fccd68e52b6dd050abb9a04eab68626e9b Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 07:08:28 -0400 Subject: [PATCH 39/41] [ADD] tui qemu: finer presets for vCPU, RAM and disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gap between 4 and 8 GB was the first one worth filling -- it is the usual size of an ERPLibre development VM -- but every neighbouring gap had the same fault: the lists jumped precisely where development VMs live. RAM now offers every gigabyte up to 16, and vCPU every count up to 16. Disk gains 30, 50, 100 and 160 G, the steps missing between those already there. Above 16 nothing changes: those jumps are deliberate, a machine that large not being sized by picking from a list. A typed value was already accepted in both interfaces, so these are a convenience, never a constraint. --- FR --- L'écart entre 4 et 8 Go était le premier à combler — c'est la taille courante d'une VM de développement ERPLibre — mais tous les écarts voisins avaient le même défaut : les listes sautaient précisément là où vivent les VM de développement. La RAM offre désormais chaque gigaoctet jusqu'à 16, et les vCPU chaque valeur jusqu'à 16. Le disque gagne 30, 50, 100 et 160 G, les paliers qui manquaient entre ceux déjà présents. Au-delà de 16, rien ne change : ces sauts sont voulus, une machine de cette taille ne se dimensionne pas en piochant dans une liste. Une valeur tapée était déjà acceptée dans les deux interfaces : ce sont donc des commodités, jamais une contrainte. Assisted-by: Claude Opus 5 --- script/todo/todo.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/script/todo/todo.py b/script/todo/todo.py index e1fe8d3..321ddf4 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -5259,10 +5259,14 @@ class TODO: # saisie commençant par un chiffre est lue comme la valeur elle-même. _QEMU_DISK_PRESETS = ( "20G", + "30G", "40G", + "50G", "60G", "80G", + "100G", "120G", + "160G", "200G", "400G", "600G", @@ -5276,8 +5280,19 @@ class TODO: _QEMU_RAM_PRESETS = ( 1024, 2048, + 3072, 4096, + 5120, + 6144, + 7168, 8192, + 9216, + 10240, + 11264, + 12288, + 13312, + 14336, + 15360, 16384, 32768, 65536, @@ -5287,7 +5302,9 @@ class TODO: # KVM autorise plus de vCPU que de cœurs (surengagement) : on prévient # plutôt que d'écrêter, contrairement au multiplicateur x1..x4 qui, lui, # est un calcul automatique et se borne aux cœurs de l'hôte. - _QEMU_CPU_PRESETS = (1, 2, 3, 4, 6, 8, 16, 24, 32) + _QEMU_CPU_PRESETS = ( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 24, 32 + ) @staticmethod def _plural(word, count): From 78282eda80686dc94c10fc2112152996d81adac1 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 17 Aug 2026 01:01:50 -0400 Subject: [PATCH 40/41] [UPD] readme: the platforms we actually support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list named Ubuntu, Debian 12, AlmaLinux, Rocky and Arch. It has been wrong for a while: install_dev.sh also dispatches Fedora, openSUSE and Linux Mint, each with its own dependency script, and the Debian path handles trixie as well as bookworm. Someone on Fedora reading this file concludes ERPLibre is not for them. Every line is now read from the code rather than remembered. Ubuntu is checked strictly against 24.04, 25.10 and 26.04, and refuses anything else; Mint against 22.3; the RHEL family arrives through ID_LIKE, hence RHEL and CentOS Stream alongside Alma and Rocky; SUSE through zypper, Leap 16.0 and Tumbleweed. Debian carries no version test, and its qpdf is built from source when the distribution ships one below 12.2 -- the same guard the three families share. The second list, further down, repeated four platforms and forgot the rest. It now says what make install_os does: detect the distribution and pick its package manager. --- FR --- La liste nommait Ubuntu, Debian 12, AlmaLinux, Rocky et Arch. Elle est fausse depuis un moment : install_dev.sh aiguille aussi Fedora, openSUSE et Linux Mint, chacune vers son script de dépendances, et le chemin Debian traite trixie autant que bookworm. Qui lit ce fichier sous Fedora en conclut qu'ERPLibre n'est pas pour lui. Chaque ligne est désormais lue dans le code plutôt que de mémoire. Ubuntu est vérifiée strictement contre 24.04, 25.10 et 26.04, et refuse le reste ; Mint contre 22.3 ; la famille RHEL arrive par ID_LIKE, d'où RHEL et CentOS Stream aux côtés d'Alma et Rocky ; SUSE par zypper, Leap 16.0 et Tumbleweed. Debian ne porte aucun test de version, et son qpdf est compilé depuis les sources quand la distribution en livre un sous 12.2 — le même garde que partagent les trois familles. La seconde liste, plus bas, répétait quatre plateformes et oubliait les autres. Elle dit maintenant ce que fait make install_os : détecter la distribution et choisir son gestionnaire de paquets. Assisted-by: Claude Opus 5 --- README.base.md | 32 ++++++++++++++++++++++---------- README.fr.md | 16 +++++++++++----- README.md | 16 +++++++++++----- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/README.base.md b/README.base.md index d79149d..ac48f4f 100644 --- a/README.base.md +++ b/README.base.md @@ -94,19 +94,31 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. # Supported platforms -- **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux -- **macOS** : via mise ou pyenv -- **Windows** : via WSL or Docker -- **Mainframe** : 390x architecture +- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 and 22.04 are dropped, pikepdf requiring a qpdf 12.2 built in C++20 +- **Linux Mint** : 22.3 +- **Debian** : 12 (bookworm) and 13 (trixie) +- **Fedora** : 41 and later +- **AlmaLinux, Rocky Linux** : 9 and 10 — RHEL and CentOS Stream take the same path +- **openSUSE** : Leap 16.0 and Tumbleweed +- **Arch Linux** : rolling release +- **macOS** : through mise or pyenv +- **Windows** : through WSL or Docker +- **Architectures** : amd64, arm64 and s390x (IBM Z mainframe) # Plateformes supportées -- **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux -- **macOS** : via mise ou pyenv -- **Windows** : via WSL ou Docker -- **Mainframe** : architecture 390x +- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 et 22.04 sont retirées, pikepdf réclamant un qpdf 12.2 bâti en C++20 +- **Linux Mint** : 22.3 +- **Debian** : 12 (bookworm) et 13 (trixie) +- **Fedora** : 41 et suivantes +- **AlmaLinux, Rocky Linux** : 9 et 10 — RHEL et CentOS Stream empruntent le même chemin +- **openSUSE** : Leap 16.0 et Tumbleweed +- **Arch Linux** : rolling release +- **macOS** : par mise ou pyenv +- **Windows** : par WSL ou Docker +- **Architectures** : amd64, arm64 et s390x (mainframe IBM Z) @@ -276,11 +288,11 @@ cd ERPLibre -Support Ubuntu 24.04, 25.10, 26.04, AlmaLinux 9+, Rocky Linux 9+ and OSX. +`make install_os` detects the distribution and picks the right dependency script: apt for Ubuntu, Linux Mint and Debian, dnf for Fedora and the RHEL family, zypper for openSUSE, pacman for Arch. See the supported platforms above. -Support Ubuntu 24.04, 25.10, 26.04, AlmaLinux 9+, Rocky Linux 9+ et OSX. +`make install_os` détecte la distribution et choisit le bon script de dépendances : apt pour Ubuntu, Linux Mint et Debian, dnf pour Fedora et la famille RHEL, zypper pour openSUSE, pacman pour Arch. Voir les plateformes supportées ci-dessus. diff --git a/README.fr.md b/README.fr.md index 5c80a26..c60c3e0 100644 --- a/README.fr.md +++ b/README.fr.md @@ -47,10 +47,16 @@ Changez de version avec `make switch_odoo_18`, `make switch_odoo_16`, etc. # Plateformes supportées -- **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux -- **macOS** : via mise ou pyenv -- **Windows** : via WSL ou Docker -- **Mainframe** : architecture 390x +- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 et 22.04 sont retirées, pikepdf réclamant un qpdf 12.2 bâti en C++20 +- **Linux Mint** : 22.3 +- **Debian** : 12 (bookworm) et 13 (trixie) +- **Fedora** : 41 et suivantes +- **AlmaLinux, Rocky Linux** : 9 et 10 — RHEL et CentOS Stream empruntent le même chemin +- **openSUSE** : Leap 16.0 et Tumbleweed +- **Arch Linux** : rolling release +- **macOS** : par mise ou pyenv +- **Windows** : par WSL ou Docker +- **Architectures** : amd64, arm64 et s390x (mainframe IBM Z) # Installation en production @@ -140,7 +146,7 @@ cd ERPLibre ``` -Support Ubuntu 24.04, 25.10, 26.04, AlmaLinux 9+, Rocky Linux 9+ et OSX. +`make install_os` détecte la distribution et choisit le bon script de dépendances : apt pour Ubuntu, Linux Mint et Debian, dnf pour Fedora et la famille RHEL, zypper pour openSUSE, pacman pour Arch. Voir les plateformes supportées ci-dessus. ```bash diff --git a/README.md b/README.md index 90bbb1b..a9f564c 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,16 @@ Switch between versions with `make switch_odoo_18`, `make switch_odoo_16`, etc. # Supported platforms -- **Linux** : Ubuntu 24.04, 25.10, 26.04; Debian 12; AlmaLinux 9+; Rocky Linux 9+; Arch Linux -- **macOS** : via mise ou pyenv -- **Windows** : via WSL or Docker -- **Mainframe** : 390x architecture +- **Ubuntu** : 24.04, 25.10, 26.04 — 20.04 and 22.04 are dropped, pikepdf requiring a qpdf 12.2 built in C++20 +- **Linux Mint** : 22.3 +- **Debian** : 12 (bookworm) and 13 (trixie) +- **Fedora** : 41 and later +- **AlmaLinux, Rocky Linux** : 9 and 10 — RHEL and CentOS Stream take the same path +- **openSUSE** : Leap 16.0 and Tumbleweed +- **Arch Linux** : rolling release +- **macOS** : through mise or pyenv +- **Windows** : through WSL or Docker +- **Architectures** : amd64, arm64 and s390x (IBM Z mainframe) # Installation in production @@ -139,7 +145,7 @@ cd ERPLibre ``` -Support Ubuntu 24.04, 25.10, 26.04, AlmaLinux 9+, Rocky Linux 9+ and OSX. +`make install_os` detects the distribution and picks the right dependency script: apt for Ubuntu, Linux Mint and Debian, dnf for Fedora and the RHEL family, zypper for openSUSE, pacman for Arch. See the supported platforms above. ```bash From 29a9e8ee20fc7d03d4c6fb04b19c502d6d33e5d1 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 17 Aug 2026 00:10:08 -0400 Subject: [PATCH 41/41] [UPD] changelog: the deployment and installation cycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Unreleased section already covered the QEMU deployment, the graphical VMs and the COW migration tools, but stopped at what existed a month ago. It says nothing of the per-VM settings, of the mise and uv providers, or of the s390x build chain -- and a reader deciding whether to upgrade has only this file to go on. Fourteen entries, spread over Added, Changed and Fixed. Nothing is repeated: the platforms and tools already listed stay where they are, and only what the entry does not already say is added. The migration work follows on its own, at the end of the branch. Entries are written on one line. Nothing wraps markdown here -- no max_line_length in .editorconfig, prettier never runs on .md -- and a sentence cut at column 80 reads worse in a diff and in a terminal. The three entries this cycle had already folded are flattened with them. Written in CHANGELOG.base.md, the only source; the two generated files come from it. mmg was unavailable here, so the generation was reproduced by a filter checked byte-for-byte against the committed output before being trusted. --- FR --- La section Unreleased couvrait déjà le déploiement QEMU, les VM graphiques et les outils de migration COW, mais s'arrêtait à l'état d'il y a un mois. Elle ne dit rien des réglages par VM, des fournisseurs mise et uv, ni de la chaîne de compilation s390x — et qui décide de mettre à jour n'a que ce fichier pour se prononcer. Quatorze entrées, réparties sur Ajouté, Modifié et Corrigé. Rien n'est redit : les plateformes et outils déjà listés restent en place, et seul ce que l'entrée ne dit pas encore est ajouté. Le travail de migration suit à part, en fin de branche. Les entrées tiennent sur une ligne. Rien n'impose de plier le markdown ici — pas de max_line_length dans .editorconfig, prettier ne tourne jamais sur du .md — et une phrase coupée à la colonne 80 se lit moins bien, en diff comme au terminal. Les trois entrées déjà pliées de ce cycle sont mises à plat avec elles. Écrit dans CHANGELOG.base.md, seule source ; les deux fichiers générés en découlent. mmg étant indisponible ici, la génération a été reproduite par un filtre vérifié à l'octet près contre la sortie déjà versionnée avant d'être employé. Assisted-by: Claude Opus 5 --- CHANGELOG.base.md | 52 +++++++++++++++++++++++++++++++---------------- CHANGELOG.fr.md | 26 ++++++++++++++++-------- CHANGELOG.md | 26 ++++++++++++++++-------- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.base.md b/CHANGELOG.base.md index 77386e5..963c3ed 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -57,12 +57,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - ERPLibre Home Mobile Application, use TODO to compile, deploy it and personalize it - Support Selenium grid from selenium_lib.py - Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor -- Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, - AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) on amd64, - arm64 and s390x, with a menu to list, - test, resize, delete and clean up -- Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the - Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch +- Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) on amd64, arm64 and s390x, with a menu to list, test, resize, delete and clean up +- Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch - Textual interfaces: install dashboard, VM deployment form, migration resume screen; Textual is installed on demand - Navigation telemetry for TODO, as a tree, a kanban or a list @@ -92,6 +88,14 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - FAQ entry on wkhtmltopdf for recent distributions - brin_advisor and brin_cluster: recommend and apply the right PostgreSQL index for an Odoo model +- Per-VM settings in the deployment form: vCPU, RAM, disk, type, ERPLibre branch, Odoo version and timezone, a lock that shields a VM from the global profile, renaming, and several copies of one entry +- Python interpreter provider chosen through EL_PYTHON_PROVIDER — mise lays down a precompiled CPython, pyenv builds one — and picked per deployment +- uv places the Python packages, pip as fallback, through EL_PIP_PROVIDER +- openSUSE Leap 16.0 and Fedora 43 in the VM catalogue +- Application store of a graphical VM: .deb only, Flatpak tooling, or snap +- Remote desktop tunnel: the SSH command is composed from ~/.ssh/config, with the hypervisor console as a third route +- The install dashboard acts on a VM without leaving it: update by parts, restart Odoo, delete; architecture column and resizable columns +- A test checks that what the tooling imports is declared @@ -111,12 +115,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Application mobile ERPLibre Home, utiliser TODO pour compiler, déployer et personnaliser - Support de la grille Selenium depuis selenium_lib.py - Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor -- Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, - Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, - Arch) en amd64, arm64 et s390x, avec - un menu pour lister, tester, redimensionner, supprimer et nettoyer -- VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le - bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch +- Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) en amd64, arm64 et s390x, avec un menu pour lister, tester, redimensionner, supprimer et nettoyer +- VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch - Interfaces Textual : tableau de bord d'installation, formulaire de déploiement de VM, écran de reprise de migration ; Textual s'installe à la demande @@ -148,6 +148,14 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Entrée de FAQ sur wkhtmltopdf pour les distributions récentes - brin_advisor et brin_cluster : recommander et appliquer le bon index PostgreSQL pour un modèle Odoo +- Réglages par VM dans le formulaire de déploiement : vCPU, RAM, disque, type, branche ERPLibre, version d'Odoo et fuseau horaire, un verrou qui soustrait une VM au profil global, le renommage, et plusieurs exemplaires d'une entrée +- Fournisseur d'interpréteur Python choisi par EL_PYTHON_PROVIDER — mise pose un CPython précompilé, pyenv en compile un — et retenu par déploiement +- uv pose les paquets Python, pip en repli, par EL_PIP_PROVIDER +- openSUSE Leap 16.0 et Fedora 43 au catalogue de VM +- Magasin d'applications d'une VM graphique : .deb seul, outillage Flatpak, ou snap +- Tunnel vers le bureau distant : la commande SSH est composée depuis ~/.ssh/config, avec la console de l'hyperviseur comme troisième voie +- Le tableau de bord d'installation agit sur une VM sans la quitter : mise à jour par parties, redémarrage d'Odoo, suppression ; colonne d'architecture et colonnes ajustables +- Un test vérifie que ce qu'importe l'outillage est bien déclaré ## Removed @@ -155,15 +163,11 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi ## Retiré -- Ubuntu 20.04 and 22.04 support, on every architecture: pikepdf needs qpdf - 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no - `g++-10` for s390x +- Ubuntu 20.04 and 22.04 support, on every architecture: pikepdf needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no `g++-10` for s390x -- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures : pikepdf - réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 - et ne publie pas de `g++-10` pour s390x +- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures : pikepdf réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 et ne publie pas de `g++-10` pour s390x ## Changed @@ -194,6 +198,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - LinuxMint 22.3 supported - Odoo 18 dependencies: flanker, orjson, python-magic, tldextract, PyYAML - Copyright year updated to 2026 +- Canadian pacman mirrors placed first on Arch, the official geographic mirror measuring four times slower from Montréal +- A Poetry dependency can be declined per architecture: factur-x is pinned to 3.x on s390x, where saxonche publishes no wheel, and PyMuPDF is set aside there @@ -220,6 +226,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - LinuxMint 22.3 pris en charge - Dépendances Odoo 18 : flanker, orjson, python-magic, tldextract, PyYAML - Année de copyright portée à 2026 +- Miroirs pacman canadiens placés en tête sur Arch, le miroir « géographique » officiel mesurant quatre fois plus lent depuis Montréal +- Une dépendance Poetry peut être déclinée par architecture : factur-x est épinglé en 3.x sur s390x, où saxonche ne publie pas de roue, et PyMuPDF y est écarté ## Fixed @@ -237,6 +245,10 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Installation on Debian 13, Fedora and Ubuntu 26.04: apt lock, wkhtmltopdf, SELinux and the missing C compiler - Documentation accents and the parallel markdown generation +- The s390x build chain, on Debian as on EL9, EL10, Fedora and openSUSE: missing compilers and headers, Rust for cryptography and bcrypt, qpdf for pikepdf, libclang for pymupdf, package names that change across releases, and a batch that failed on one unknown name without ever naming it +- A graphical desktop install froze for thirty minutes on a snap package that could not reach the store +- The compiler was killed for lack of memory while building CPython on a small s390x guest +- The README listed neither Fedora, openSUSE, Linux Mint nor Debian 13, all of them supported @@ -250,6 +262,10 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Installation sur Debian 13, Fedora et Ubuntu 26.04 : verrou apt, wkhtmltopdf, SELinux et le compilateur C manquant - Accents de la documentation et génération markdown en parallèle +- La chaîne de compilation s390x, sur Debian comme sur EL9, EL10, Fedora et openSUSE : compilateurs et en-têtes manquants, Rust pour cryptography et bcrypt, qpdf pour pikepdf, libclang pour pymupdf, des noms de paquets qui changent d'une version à l'autre, et un lot en échec sur un seul nom inconnu sans jamais le nommer +- L'installation d'un bureau graphique figeait trente minutes sur un paquet snap qui ne joignait pas le magasin +- Le compilateur était tué faute de mémoire en bâtissant CPython sur une petite VM s390x +- Le README ne listait ni Fedora, ni openSUSE, ni Linux Mint, ni Debian 13, toutes supportées ## Security diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 040fc6c..d04e4ed 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -31,12 +31,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Application mobile ERPLibre Home, utiliser TODO pour compiler, déployer et personnaliser - Support de la grille Selenium depuis selenium_lib.py - Ajout des addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor -- Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, - Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, - Arch) en amd64, arm64 et s390x, avec - un menu pour lister, tester, redimensionner, supprimer et nettoyer -- VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le - bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch +- Déploiement de VM ERPLibre en QEMU/KVM depuis des images cloud (Ubuntu, Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) en amd64, arm64 et s390x, avec un menu pour lister, tester, redimensionner, supprimer et nettoyer +- VM graphiques : un choix serveur / bureau proposant GNOME ou Cinnamon (le bureau de Linux Mint), avec accès distant par xrdp — TigerVNC sur Arch - Interfaces Textual : tableau de bord d'installation, formulaire de déploiement de VM, écran de reprise de migration ; Textual s'installe à la demande @@ -68,12 +64,18 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Entrée de FAQ sur wkhtmltopdf pour les distributions récentes - brin_advisor et brin_cluster : recommander et appliquer le bon index PostgreSQL pour un modèle Odoo +- Réglages par VM dans le formulaire de déploiement : vCPU, RAM, disque, type, branche ERPLibre, version d'Odoo et fuseau horaire, un verrou qui soustrait une VM au profil global, le renommage, et plusieurs exemplaires d'une entrée +- Fournisseur d'interpréteur Python choisi par EL_PYTHON_PROVIDER — mise pose un CPython précompilé, pyenv en compile un — et retenu par déploiement +- uv pose les paquets Python, pip en repli, par EL_PIP_PROVIDER +- openSUSE Leap 16.0 et Fedora 43 au catalogue de VM +- Magasin d'applications d'une VM graphique : .deb seul, outillage Flatpak, ou snap +- Tunnel vers le bureau distant : la commande SSH est composée depuis ~/.ssh/config, avec la console de l'hyperviseur comme troisième voie +- Le tableau de bord d'installation agit sur une VM sans la quitter : mise à jour par parties, redémarrage d'Odoo, suppression ; colonne d'architecture et colonnes ajustables +- Un test vérifie que ce qu'importe l'outillage est bien déclaré ## Retiré -- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures : pikepdf - réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 - et ne publie pas de `g++-10` pour s390x +- Le support d'Ubuntu 20.04 et 22.04, sur toutes les architectures : pikepdf réclame qpdf 12.2, dont la compilation exige C++20, quand focal livre GCC 9 et ne publie pas de `g++-10` pour s390x ## Modifié @@ -100,6 +102,8 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - LinuxMint 22.3 pris en charge - Dépendances Odoo 18 : flanker, orjson, python-magic, tldextract, PyYAML - Année de copyright portée à 2026 +- Miroirs pacman canadiens placés en tête sur Arch, le miroir « géographique » officiel mesurant quatre fois plus lent depuis Montréal +- Une dépendance Poetry peut être déclinée par architecture : factur-x est épinglé en 3.x sur s390x, où saxonche ne publie pas de roue, et PyMuPDF y est écarté ## Corrigé @@ -113,6 +117,10 @@ Recréer l'environnement virtuel, utiliser le guide d'installation depuis l'outi - Installation sur Debian 13, Fedora et Ubuntu 26.04 : verrou apt, wkhtmltopdf, SELinux et le compilateur C manquant - Accents de la documentation et génération markdown en parallèle +- La chaîne de compilation s390x, sur Debian comme sur EL9, EL10, Fedora et openSUSE : compilateurs et en-têtes manquants, Rust pour cryptography et bcrypt, qpdf pour pikepdf, libclang pour pymupdf, des noms de paquets qui changent d'une version à l'autre, et un lot en échec sur un seul nom inconnu sans jamais le nommer +- L'installation d'un bureau graphique figeait trente minutes sur un paquet snap qui ne joignait pas le magasin +- Le compilateur était tué faute de mémoire en bâtissant CPython sur une petite VM s390x +- Le README ne listait ni Fedora, ni openSUSE, ni Linux Mint, ni Debian 13, toutes supportées ## Sécurité diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d4b996..300e02f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,12 +31,8 @@ Recreating the virtual environment, use installation guide from tool `make`. - ERPLibre Home Mobile Application, use TODO to compile, deploy it and personalize it - Support Selenium grid from selenium_lib.py - Add addons OnlyOffice, Cetmix, OCA automation, OCA shopfloor -- Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, - AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) on amd64, - arm64 and s390x, with a menu to list, - test, resize, delete and clean up -- Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the - Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch +- Deploy ERPLibre VMs with QEMU/KVM from cloud images (Ubuntu, Debian, Fedora, AlmaLinux, Rocky Linux, openSUSE Tumbleweed, Arch) on amd64, arm64 and s390x, with a menu to list, test, resize, delete and clean up +- Graphical VMs: a server / desktop choice offering GNOME or Cinnamon (the Linux Mint desktop), with remote access through xrdp — TigerVNC on Arch - Textual interfaces: install dashboard, VM deployment form, migration resume screen; Textual is installed on demand - Navigation telemetry for TODO, as a tree, a kanban or a list @@ -66,12 +62,18 @@ Recreating the virtual environment, use installation guide from tool `make`. - FAQ entry on wkhtmltopdf for recent distributions - brin_advisor and brin_cluster: recommend and apply the right PostgreSQL index for an Odoo model +- Per-VM settings in the deployment form: vCPU, RAM, disk, type, ERPLibre branch, Odoo version and timezone, a lock that shields a VM from the global profile, renaming, and several copies of one entry +- Python interpreter provider chosen through EL_PYTHON_PROVIDER — mise lays down a precompiled CPython, pyenv builds one — and picked per deployment +- uv places the Python packages, pip as fallback, through EL_PIP_PROVIDER +- openSUSE Leap 16.0 and Fedora 43 in the VM catalogue +- Application store of a graphical VM: .deb only, Flatpak tooling, or snap +- Remote desktop tunnel: the SSH command is composed from ~/.ssh/config, with the hypervisor console as a third route +- The install dashboard acts on a VM without leaving it: update by parts, restart Odoo, delete; architecture column and resizable columns +- A test checks that what the tooling imports is declared ## Removed -- Ubuntu 20.04 and 22.04 support, on every architecture: pikepdf needs qpdf - 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no - `g++-10` for s390x +- Ubuntu 20.04 and 22.04 support, on every architecture: pikepdf needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and publishes no `g++-10` for s390x ## Changed @@ -98,6 +100,8 @@ Recreating the virtual environment, use installation guide from tool `make`. - LinuxMint 22.3 supported - Odoo 18 dependencies: flanker, orjson, python-magic, tldextract, PyYAML - Copyright year updated to 2026 +- Canadian pacman mirrors placed first on Arch, the official geographic mirror measuring four times slower from Montréal +- A Poetry dependency can be declined per architecture: factur-x is pinned to 3.x on s390x, where saxonche publishes no wheel, and PyMuPDF is set aside there ## Fixed @@ -111,6 +115,10 @@ Recreating the virtual environment, use installation guide from tool `make`. - Installation on Debian 13, Fedora and Ubuntu 26.04: apt lock, wkhtmltopdf, SELinux and the missing C compiler - Documentation accents and the parallel markdown generation +- The s390x build chain, on Debian as on EL9, EL10, Fedora and openSUSE: missing compilers and headers, Rust for cryptography and bcrypt, qpdf for pikepdf, libclang for pymupdf, package names that change across releases, and a batch that failed on one unknown name without ever naming it +- A graphical desktop install froze for thirty minutes on a snap package that could not reach the store +- The compiler was killed for lack of memory while building CPython on a small s390x guest +- The README listed neither Fedora, openSUSE, Linux Mint nor Debian 13, all of them supported ## Security