diff --git a/.claude/rules/08-deployment.md b/.claude/rules/08-deployment.md index d7f7935..e4ad023 100644 --- a/.claude/rules/08-deployment.md +++ b/.claude/rules/08-deployment.md @@ -6,4 +6,35 @@ - **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+, openSUSE Leap 16 et Tumbleweed, +Arch Linux, +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. + +## 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/CHANGELOG.base.md b/CHANGELOG.base.md index 665c8ce..963c3ed 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -57,9 +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, - Arch) on amd64, arm64 and s390x, with a menu to list, test, resize, delete - and clean up +- 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 @@ -89,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 @@ -108,9 +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, Arch) en amd64, arm64 et s390x, avec un menu pour lister, - tester, redimensionner, supprimer et nettoyer +- 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 @@ -142,6 +148,26 @@ 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 + +## 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 @@ -172,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 @@ -198,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 @@ -215,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 @@ -228,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 e93f828..d04e4ed 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -31,9 +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, Arch) en amd64, arm64 et s390x, avec un menu pour lister, - tester, redimensionner, supprimer et nettoyer +- 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 @@ -65,6 +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 ## Modifié @@ -91,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é @@ -104,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 103f0ea..300e02f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +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, - Arch) on amd64, arm64 and s390x, with a menu to list, test, resize, delete - and clean up +- 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 @@ -63,6 +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 ## Changed @@ -89,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 @@ -102,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 diff --git a/Makefile b/Makefile index 465190a..6b7e893 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,47 @@ 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. +# 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 \ + 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 b07de49..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 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux -- **macOS** : via 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 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux -- **macOS** : via 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 20.04, 22.04, 24.04, 25.04 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 20.04, 22.04, 24.04, 25.04 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 bd81c44..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 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux -- **macOS** : via 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 20.04, 22.04, 24.04, 25.04 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 10191a4..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 20.04, 22.04, 24.04, 25.04; Debian 12; Arch Linux -- **macOS** : via 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 20.04, 22.04, 24.04, 25.04 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 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/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/env_var.sh b/env_var.sh index c899ad9..179fafc 100755 --- a/env_var.sh +++ b/env_var.sh @@ -14,6 +14,17 @@ 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}" +# 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/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/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/install/install_debian_dependency.sh b/script/install/install_debian_dependency.sh index cc56946..61e759b 100755 --- a/script/install/install_debian_dependency.sh +++ b/script/install/install_debian_dependency.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash . ./env_var.sh +. ./script/install/lib_qpdf.sh +. ./script/install/lib_lowmem.sh EL_USER=${USER} #EL_INSTALL_WKHTMLTOPDF="True" @@ -19,22 +21,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 +61,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,7 +73,144 @@ 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 + # 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. + ${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 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 + # -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 #-------------------------------------------------- @@ -66,16 +218,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 @@ -119,14 +261,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=$? @@ -148,24 +282,100 @@ ${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 +# 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 +# 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 + +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 -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 - -sudo npm install npm@latest -g -retVal=$? -if [[ $retVal -ne 0 ]]; then - echo "npm install npm lastest installation error." +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 diff --git a/script/install/install_dev.sh b/script/install/install_dev.sh index 95cc0a3..5f3228d 100755 --- a/script/install/install_dev.sh +++ b/script/install/install_dev.sh @@ -1,13 +1,32 @@ #!/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 -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 - 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 @@ -20,14 +39,26 @@ 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 - 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, 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_fedora_dependency.sh b/script/install/install_fedora_dependency.sh index 7fbafa7..eb571e4 100755 --- a/script/install/install_fedora_dependency.sh +++ b/script/install/install_fedora_dependency.sh @@ -2,25 +2,159 @@ # © 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 +. ./script/install/lib_lowmem.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 +#-------------------------------------------------- +# 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, 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 +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." + # /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 ----" + # 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). + # « 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_locally.sh b/script/install/install_locally.sh index 350b86a..d73d13d 100755 --- a/script/install/install_locally.sh +++ b/script/install/install_locally.sh @@ -19,15 +19,29 @@ 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 +# Bornage des compilations : voir el_build_limit_jobs. +. ./script/install/lib_lowmem.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 @@ -50,10 +64,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 @@ -61,7 +86,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 @@ -81,28 +107,87 @@ if [[ "${EL_PHASE}" != "setup" ]]; then exit 1 fi - # Delete artifacts created by pip, cause error in next "poetry install" - if [[ ! -f "${POETRY_ODOO_PATH}" ]]; then + # 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 + + # 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 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 + # 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 + # « 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/install_suse_dependency.sh b/script/install/install_suse_dependency.sh new file mode 100755 index 0000000..40eeed8 --- /dev/null +++ b/script/install/install_suse_dependency.sh @@ -0,0 +1,370 @@ +#!/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 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 +. ./script/install/lib_lowmem.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. +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) +#-------------------------------------------------- +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 + # 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 + # 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. +# 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 \ + 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/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_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)." +} 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 "$@" +} 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/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}" +} 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) diff --git a/script/qemu/README.base.md b/script/qemu/README.base.md index 116a496..b031ad7 100644 --- a/script/qemu/README.base.md +++ b/script/qemu/README.base.md @@ -173,16 +173,73 @@ 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): + +| Distro | Versions | amd64 | arm64 | s390x | +|---|---|:-:|:-:|:-:| +| ubuntu | `24.04` (default), `25.10`, `26.04` | ✔ | ✔ | ✔ | +| debian | `11`, `12` (default), `13` | ✔ | ✔ | — | +| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | +| almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | +| rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | +| 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` 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. + +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é) : + +| 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` | ✔ | ✔ | `43` seule | +| almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | +| rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | +| 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` 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. + +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 afae765..af7b826 100644 --- a/script/qemu/README.fr.md +++ b/script/qemu/README.fr.md @@ -92,9 +92,39 @@ 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é) : + +| 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` | ✔ | ✔ | `43` seule | +| almalinux | `9` (défaut), `10` | ✔ | ✔ | ✔ | +| rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ | +| 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` 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. + +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 f4d8b02..473a847 100644 --- a/script/qemu/README.md +++ b/script/qemu/README.md @@ -86,9 +86,36 @@ 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): + +| Distro | Versions | amd64 | arm64 | s390x | +|---|---|:-:|:-:|:-:| +| ubuntu | `24.04` (default), `25.10`, `26.04` | ✔ | ✔ | ✔ | +| debian | `11`, `12` (default), `13` | ✔ | ✔ | — | +| fedora | `41`, `42` (default), `43`, `44` | ✔ | ✔ | `43` only | +| almalinux | `9` (default), `10` | ✔ | ✔ | ✔ | +| rocky | `9`, `10` (default) | ✔ | ✔ | ✔ | +| 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` 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. + +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 f8f570b..4f483de 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,37 @@ 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"), +} +# 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). ARCH_VERSIONS: dict[str, tuple[str, str, int, str]] = { "latest": ("latest", "archlinux", 1024, "20G"), @@ -107,6 +142,9 @@ 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"), + "opensuse": (OPENSUSE_VERSIONS, "16.0"), "arch": (ARCH_VERSIONS, "latest"), } @@ -114,6 +152,9 @@ 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"}, + "opensuse": {"amd64": "x86_64", "arm64": "aarch64"}, "arch": {"amd64": "x86_64", "arm64": "aarch64"}, } @@ -126,8 +167,43 @@ 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") +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 +# 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", + "fedora", + "almalinux", + "rocky", + "opensuse", +) + + # arch générique -> distros la publiant (pour valider --arch tôt). ARCH_DISTRO_SUPPORT: dict[str, tuple[str, ...]] = { "s390x": S390X_DISTROS, @@ -162,11 +238,23 @@ 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" +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 # 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 @@ -212,6 +300,43 @@ 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 == "opensuse": + # 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}/distribution/leap/{version}/appliances/" + f"Leap-{version}-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"] @@ -229,12 +354,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/" @@ -265,6 +396,16 @@ 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": + 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 + # (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" @@ -1287,6 +1428,27 @@ def host_timezone() -> str: return "UTC" +def user_groups(distro: str) -> str: + """Groupes secondaires du compte créé par cloud-init. + + 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( args: argparse.Namespace, pw_hash: str | None, ssh_keys: list[str] ) -> str: @@ -1297,10 +1459,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: {user_groups(args.distro)}", " shell: /bin/bash", " lock_passwd: false" if pw_hash else " lock_passwd: true", ] @@ -1606,6 +1773,37 @@ 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: + # 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", # Sans --connect, un utilisateur non root vise qemu:///session : le @@ -1635,7 +1833,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 à @@ -1644,6 +1842,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). @@ -1858,6 +2057,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)." ) @@ -2084,6 +2290,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/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 76df668..d86f5e7 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: @@ -35,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): @@ -63,7 +95,46 @@ def disk_gb(value) -> int: return int(parsed[:-1]) if parsed else 0 -def apply_profile(entries, profile, base_vcpus, host_cpu, custom=None): +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 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 @@ -74,9 +145,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 @@ -91,6 +162,19 @@ 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, + # 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 @@ -105,13 +189,58 @@ def apply_overrides(vms, entries, overrides): return vms -def build_vms(entries, profile, base_vcpus, host_cpu, custom, overrides): +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. + + 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="", + suffixes=None, +): """Catalogue choisi + profil + surcharges -> liste de VM de la spec.""" - return apply_overrides( - apply_profile(entries, profile, base_vcpus, host_cpu, custom), + 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, 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 def vm_status(name, domains): @@ -164,6 +293,9 @@ 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", ""), + "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"], @@ -201,11 +333,9 @@ def run_deploy_form(ctx, run_app: bool = True): from textual.widgets import ( Button, Checkbox, - DataTable, Footer, Header, Input, - Label, RadioButton, RadioSet, Select, @@ -213,6 +343,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 []) @@ -222,10 +358,37 @@ 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 + # [(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 + # 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} 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__" + # « 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"), + "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 "" @@ -234,46 +397,34 @@ 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.""" + class RenameScreen(ModalScreen): + """Nom d'une VM. Vide = revenir au nom automatique.""" BINDINGS = [("escape", "cancel", t("Cancel"))] - def __init__(self, vm): + def __init__(self, name, auto): super().__init__() - self._vm = vm + self._name = name + self._auto = auto 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") + 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 != "e_ok": + if event.button.id != "ren_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) + self.dismiss(self.query_one("#renval", Input).value) def action_cancel(self) -> None: self.dismiss(None) @@ -306,19 +457,58 @@ 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; } RadioSet { height: auto; layout: horizontal; } + .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; + } + /* 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; } + /* « 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; } - EditVMScreen { align: center middle; } - #editbox { - width: 56; height: auto; padding: 1 2; + RenameScreen { align: center middle; } + #renbox { + width: 60; height: auto; padding: 1 2; border: thick $accent; background: $surface; } - #edittitle { color: $accent; text-style: bold; } - #editbtns { height: auto; padding-top: 1; } + #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; @@ -332,8 +522,9 @@ 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")), + ("f9", "dump_state", t("Diagnostic dump")), ("f6", "select_all", t("All")), ("f7", "select_main", t("Main versions")), ("f8", "select_none", t("None")), @@ -345,7 +536,29 @@ 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 = {} + # 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 + # 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 = [] @@ -361,32 +574,87 @@ 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") 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"] - ], - prompt=t("RAM (MB)"), + ] + + [(t("free value…"), FREE)], + prompt=t("RAM: 2048 or 8G"), id="f_ram", disabled=True, ) + yield Input( + placeholder=t("RAM: 2048 or 8G"), + 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( + 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 (default):"), classes="grouptitle") + with RadioSet(id="f_type"): + yield RadioButton( + t("Server (no graphical interface)"), + value=not defaults.get("desktop", ""), + ) + for key, label in desktops: + yield RadioButton( + 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"), @@ -403,7 +671,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", ) @@ -418,10 +686,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( @@ -434,35 +715,52 @@ 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, + # 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"): - 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) # -- 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, []) @@ -499,7 +797,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, @@ -507,34 +805,424 @@ def run_deploy_form(ctx, run_app: bool = True): host_cpu, self.custom, self.overrides, + self._default_desktop(), + desktop_suffixes, ) - 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 + 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 + # 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): + 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() + + 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 _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) + + 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 + 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.""" + 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] + + # -- 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._plan_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"] + 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 + # 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)], + 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", + ), + ( + Button("−", id=f"m{i}", classes="vmcopy") + if item.get("instance") + else Static("", classes="vmcopy") + ), + 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=( + self.rows[i]["vm"].get("branch") + if i < len(self.rows) + else "" + ) + or self._branch(), + 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, + allow_blank=False, + id=f"v{i}_type", + ), + classes="vmrow", + ) + widgets.append( + Vertical( + Static(self._row_head(i, r), id=f"h{i}"), + row, + # 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" + ), + ) + ) + + # 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 _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()), + (f"#v{i}_prof", self._row_profile_index(i)), + ): + 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"] + 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 + # 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 + + 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']}" + 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}" + ) def _render_plan(self): - table = self.query_one("#plan", DataTable) - table.clear() - for r in self.rows: - vm = r["vm"] - icon = {"new": "", "exists": "⏭ ", "orphan": "❌ "}[r["state"]] - table.add_row( - vm["name"], - vm["distro"], - vm["version"], - vm["arch"], - str(vm["vcpus"]), - f"{vm['ram']}Mo", - f"{r['disk_gb']}G", - f"{icon}{r['note']}", - ) + # 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 + 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. @@ -555,9 +1243,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"{warn}{dup_txt}" + f"{skip_txt}{warn}{dup_txt}" ) # -- réactions aux champs -------------------------------------- # @@ -565,28 +1264,418 @@ 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._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 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 + self._show_free(field, custom and self._free.get(field)) self._recompute() + # -- 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 _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 + index = int(match.group(1)) + return index if 0 <= index < len(self.rows) else None + + def _row_key(self, index): + entries = self._plan_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 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: + # 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) + if key is None: + return + 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) + if not self.overrides.get(key): + self.overrides.pop(key, None) + 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) + 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) - if field: - if event.value is not Select.BLANK: - self.custom[field] = event.value + 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 == "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. + 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 ""): + 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 + # 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 == "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",) + if event.select.id == "f_branch" + else ("install_cmd",) + ) + self._recompute() + return + 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._clear_overrides((field,)) + 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 "" + elif field == "ram": + 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: + 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 _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() + ), + "install_label": ( + vm.get("install_label") + or ( + profiles[self._row_profile_index(index)][0] + if profiles + else "" + ) + ), + } + 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 + elif event.checkbox.id == "f_par_all": + self.query_one("#f_par", Select).disabled = event.value # -- actions ---------------------------------------------------- # def action_select_all(self) -> None: @@ -603,20 +1692,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)): + def action_clear_vm(self) -> None: + """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 - 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) + key = self._row_key(index) + if key is None or key not in self.overrides: + return + self.overrides.pop(key) + # 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 @@ -643,9 +1733,19 @@ 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._default_desktop(), + "python_provider": self._python_provider(), + "app_store": self._app_store(), "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: @@ -656,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/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index 7b68617..eb63c5c 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: @@ -98,6 +103,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 +144,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,25 +186,41 @@ 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'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*) 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'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, @@ -212,7 +263,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"], @@ -234,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 @@ -261,6 +332,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. @@ -731,13 +842,134 @@ 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. +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()) @@ -786,6 +1018,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": "⏳", @@ -795,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; } @@ -808,6 +1128,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é)"), @@ -817,6 +1152,14 @@ 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"), + # 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"), ] @@ -824,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 @@ -873,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 "?", "", "⏳", "—", @@ -1041,7 +1402,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", "🗑") @@ -1051,7 +1419,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. @@ -1096,7 +1468,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) @@ -1433,6 +1806,180 @@ 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 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) + 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: + """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 _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) + + 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 ».""" 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 diff --git a/script/todo/todo.py b/script/todo/todo.py index 0d9fa62..321ddf4 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 @@ -1022,12 +1023,15 @@ 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"), + # 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"), } @@ -1075,8 +1079,21 @@ 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", + "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"} @@ -1243,6 +1260,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,17 +1277,17 @@ 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( "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")}, ] @@ -1286,26 +1308,28 @@ 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": self._qemu_ssh_config_menu() elif status == "14": + self._qemu_tunnel_menu() + elif status == "15": self._qemu_list_images() else: cmd_no_found = True @@ -1457,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. @@ -2137,20 +2313,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:')}") @@ -2367,16 +2557,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.""" @@ -3766,6 +4002,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).""" @@ -4235,11 +4479,501 @@ 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é.""" + # 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 = { + "gnome": { + "label": "GNOME", + "apt": "gnome-core dbus-x11", + "dnf_env": "graphical-server-environment " + "workstation-product-environment gnome-desktop", + "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)", + # 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", + "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 + # 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",) + + # 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 + 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"}, + "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. + 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, 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 + 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. 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 + # 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", + ) + + # 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) + # 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 ( + ". /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 ' + "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; ' + ) + + @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, + 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 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"{de['apt']} {rem['apt']['packages']} " + 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, + # 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; " + + 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; " + # « --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. + "sudo systemctl set-default graphical.target || 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 " + "sudo systemctl enable --now xrdp >/dev/null 2>&1 || true; " + f'echo " {t("Remote desktop:")} RDP 3389"; ' + # 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)"; ' + + self._qemu_tunnel_hint(5901, "VNC") + + "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, + 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 + 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. + `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, + # 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, 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}" # Profils AVEC Odoo (install_odoo*) uniquement : après l'install, on @@ -4263,44 +4997,21 @@ 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, app_store) 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. - "command -v cloud-init >/dev/null 2>&1 && " - "sudo timeout 900 cloud-init status --wait >/dev/null 2>&1 " - "|| true; " + "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(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 @@ -4328,28 +5039,48 @@ 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 », + # é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à. + # + # 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;; " + "*) 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 " "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 || " '{ 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. + ( @@ -4373,7 +5104,15 @@ 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, + 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. @@ -4385,7 +5124,27 @@ class TODO: run_monitor, ) - remote = self._qemu_erplibre_remote_cmd(branch, final_cmd, prod) + # `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 {} + # 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 + # 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, + cmd_def, + prod, + "" if desk_map else desktop, + python_provider, + app_store, + ) try: mod = self._qemu_import_module() except Exception: @@ -4401,21 +5160,31 @@ 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 or branch_map or cmd_map: + entry["remote_cmd"] = self._qemu_erplibre_remote_cmd( + branch_map.get(name, branch_def), + cmd_map.get(name, cmd_def), + prod, + desk_map.get(name, ""), + python_provider, + app_store, + ) + vms.append(entry) else: print(f" {name}: {t('no IP, skipped.')}") 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). @@ -4441,7 +5210,16 @@ 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, + 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 ; @@ -4462,7 +5240,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, python_provider, app_store + ) ssh_opts = ( "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " "-o ConnectTimeout=15" @@ -4479,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", @@ -4496,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, @@ -4507,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, 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): @@ -4816,10 +5613,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)}") @@ -4829,12 +5649,36 @@ 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')}") + flavour = spec.get("desktop") + if flavour: + label = (self._QEMU_DESKTOP.get(flavour) or {}).get( + "label", flavour + ) + 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") @@ -4856,6 +5700,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).""" @@ -4889,9 +5734,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 @@ -4916,6 +5770,9 @@ class TODO: dry_run=dry_run, timezone=spec.get("timezone"), locale=spec.get("locale"), + # 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"))), ) # ---------------------------------------------------------------- # @@ -4941,10 +5798,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, @@ -5143,6 +6007,17 @@ 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, + "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": [ + (k, v["label"]) for k, v in self._QEMU_DESKTOP.items() + ], "defaults": { "install": True, "add_ssh_config": True, @@ -5170,6 +6045,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() @@ -5409,6 +6287,74 @@ class TODO: ) print(f" ⚠ {warn}") + def _qemu_ask_desktop(self): + """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)')} *") + 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 "" + + @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). + + `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 @@ -5467,6 +6413,21 @@ 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. + # 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] + ) # 4) Option : installer ERPLibre dans ~/git/erplibre de chaque VM. install = None @@ -5513,17 +6474,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, @@ -5532,6 +6504,9 @@ class TODO: "ssh_key": ssh_key, "timezone": timezone, "locale": locale, + "desktop": desktop, + "python_provider": python_provider, + "app_store": app_store, "install": install, "add_ssh_config": add_ssh_config, "parallelism": parallelism, @@ -5599,6 +6574,35 @@ class TODO: deployed = list(spec.get("existing") or []) install = spec.get("install") install_branch = install["branch"] if install else None + # 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 "" + 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 + 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"] @@ -5640,7 +6644,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) } @@ -5657,16 +6663,22 @@ 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, + branch_map if branch_multi else install_branch, ip_map, - install["cmd"], - install["prod"], + cmd_map if cmd_multi else base_cmd, + install["prod"] if install else False, + desktop=desktop_map, + python_provider=python_provider, + app_store=app_store, ) else: print( @@ -5681,6 +6693,9 @@ class TODO: ip_map.get(name), install["cmd"], 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 diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index cea0ecd..b4e69f7 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1973,6 +1973,140 @@ 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:", + }, + "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): ", + }, + "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:", + }, + "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)", + }, + "server": { + "fr": "serveur", + "en": "server", + }, + "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): ", + }, + "Installing the GNOME desktop (long)": { + "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:", + }, + "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…", + }, + "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", @@ -2005,6 +2139,148 @@ 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"}, + "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", + }, "Preview": { "fr": "Aperçu", "en": "Preview", @@ -2786,8 +3062,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.", @@ -3414,8 +3690,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)": { 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 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()