Metadata-Version: 2.4
Name: adstoolbox
Version: 2026.9.2
Summary: Generic functions
License-Expression: MIT
Author: Olivier Siguré
Author-email: olivier.sigure@alchimiedatasolutions.com
Requires-Python: >=3.10,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Provides-Extra: all
Provides-Extra: cdc
Provides-Extra: dataframe
Provides-Extra: files
Provides-Extra: git
Provides-Extra: google
Provides-Extra: mssql
Provides-Extra: mysql
Provides-Extra: pgsql
Requires-Dist: GitPython (>=3.1.43,<4.0) ; extra == "all"
Requires-Dist: GitPython (>=3.1.43,<4.0) ; extra == "git"
Requires-Dist: PyGithub (>=2.8,<3.0) ; extra == "all"
Requires-Dist: PyGithub (>=2.8,<3.0) ; extra == "git"
Requires-Dist: SQLAlchemy (>=2.0,<3.0) ; extra == "all"
Requires-Dist: SQLAlchemy (>=2.0,<3.0) ; extra == "cdc"
Requires-Dist: adlfs (>=2024.1,<2026.0) ; extra == "all"
Requires-Dist: adlfs (>=2024.1,<2026.0) ; extra == "files"
Requires-Dist: chardet (>=5.2,<6.0)
Requires-Dist: fsspec (>=2024.6,<2026.0) ; extra == "all"
Requires-Dist: fsspec (>=2024.6,<2026.0) ; extra == "files"
Requires-Dist: google-api-python-client (>=2.149,<3.0) ; extra == "all"
Requires-Dist: google-api-python-client (>=2.149,<3.0) ; extra == "google"
Requires-Dist: google-auth (>=2.30,<3.0) ; extra == "all"
Requires-Dist: google-auth (>=2.30,<3.0) ; extra == "google"
Requires-Dist: google-auth-oauthlib (>=1.2,<2.0) ; extra == "all"
Requires-Dist: google-auth-oauthlib (>=1.2,<2.0) ; extra == "google"
Requires-Dist: jsonschema (>=4.23,<5.0) ; extra == "all"
Requires-Dist: jsonschema (>=4.23,<5.0) ; extra == "cdc"
Requires-Dist: paramiko (>=3.4,<4.0) ; extra == "all"
Requires-Dist: paramiko (>=3.4,<4.0) ; extra == "files"
Requires-Dist: polars (>=1.33,<2.0) ; extra == "all"
Requires-Dist: polars (>=1.33,<2.0) ; extra == "dataframe"
Requires-Dist: polars (>=1.33,<2.0) ; extra == "google"
Requires-Dist: polars (>=1.33,<2.0) ; extra == "mssql"
Requires-Dist: polars (>=1.33,<2.0) ; extra == "mysql"
Requires-Dist: polars (>=1.33,<2.0) ; extra == "pgsql"
Requires-Dist: psycopg2-binary (>=2.9,<3.0) ; extra == "all"
Requires-Dist: psycopg2-binary (>=2.9,<3.0) ; extra == "pgsql"
Requires-Dist: pymssql (>=2.3,<3.0) ; extra == "all"
Requires-Dist: pymssql (>=2.3,<3.0) ; extra == "mssql"
Requires-Dist: pymysql (>=1.1,<2.0) ; extra == "all"
Requires-Dist: pymysql (>=1.1,<2.0) ; extra == "mysql"
Requires-Dist: python-dotenv (>=1.0,<2.0)
Requires-Dist: requests (>=2.32,<3.0)
Requires-Dist: smbprotocol (>=1.11,<2.0) ; extra == "all"
Requires-Dist: smbprotocol (>=1.11,<2.0) ; extra == "files"
Requires-Dist: tzdata (>=2025.2) ; sys_platform == "win32"
Description-Content-Type: text/markdown

# Alchimie Data Solutions — adsToolBox

`adsToolBox` est une librairie Python interne d'**Alchimie Data Solutions**, qui regroupe les
fonctions génériques réutilisées dans les développements liés à **Onyx**. Elle fournit des
briques homogènes pour accéder aux bases de données, industrialiser des pipelines, capturer
du changement (CDC), manipuler des fichiers sur différents protocoles, et gérer les tâches
transverses (logs, chrono, environnement, mails, Git, Odoo, Google Calendar).

> **Dépôt privé** — ce repository est réservé aux employés d'Alchimie Data Solutions.
> Le package est toutefois publié publiquement sur PyPI sous le nom
> [`adstoolbox`](https://pypi.org/project/adstoolbox/), et un dépôt d'exemples publics
> est disponible : [AlchimieDataSolutions/DemoPy](https://github.com/AlchimieDataSolutions/DemoPy).

- **Nom du package** : `adstoolbox`
- **Module Python** : `adsToolBox`
- **Versioning** : calendaire (`YYYY.MM.DD`) — voir `pyproject.toml`
- **Python** : `>= 3.10, < 4.0`
- **Licence** : MIT

## Sommaire

- [Fonctionnalités](#fonctionnalités)
- [Installation](#installation)
- [Modules et extras](#modules-et-extras)
- [Démarrage rapide](#démarrage-rapide)
- [Structure du dépôt](#structure-du-dépôt)
- [Tests](#tests)
- [Développement](#développement)
- [Dépendances](#dépendances)
- [Auteurs](#auteurs)
- [Licence](#licence)

## Fonctionnalités

Tous les symboles ci-dessous sont exposés directement depuis `adsToolBox` (voir `adsToolBox/__init__.py`).
La colonne **Extra** indique la dépendance optionnelle à installer — voir
[Modules et extras](#modules-et-extras).

### Bases de données et pipelines

| Symbole | Extra | Rôle |
|---|---|---|
| `DataFactory` | `dataframe` | Classe abstraite commune : `connect`, `sql_query`, `sql_exec`, `sql_scalaire`, `insert`, `insert_many`, `insert_bulk`, `upsert`, `upsert_many`, `upsert_bulk`, `find_text_anywhere`… |
| `DbMssql` | `mssql` | Implémentation SQL Server (driver `pymssql`). |
| `DbMysql` | `mysql` | Implémentation MySQL (driver `pymysql`). |
| `DbPgsql` | `pgsql` | Implémentation PostgreSQL (driver `psycopg2`). |
| `Pipeline` | `dataframe` | Orchestration d'un transfert source → destination avec batch, déduplication par hash, inférence de schéma Polars, création automatique de la table cible. |
| `DataComparator` | `dataframe` | Compare deux sources batch par batch et produit un rapport de différences. |
| `ChangeDataCapture` | `cdc` | CDC déclarative (modes `append`, `scd1`, `scd2`, `scd4`) validée par JSON Schema, avec gestion staging/persistent et synchronisation côté métier. |

### Fichiers, infrastructure, intégrations externes

| Symbole | Extra | Rôle |
|---|---|---|
| `FileHandler` | `files` | Accès fichiers multi-backends via `fsspec` (local, SMB, SFTP, Azure Blob) avec transfert atomique et checksum optionnel. |
| `GitHandler` | `git` | Clonage / mise à jour de dépôts Git via token (`GitPython` + API GitHub). Exige aussi le binaire `git` dans le `PATH`. |
| `GoogleCalendarConnector` | `google` | Lecture/écriture d'événements Google Calendar (OAuth2). |
| `MailReader` | — | Lecture IMAP avec décodage robuste des en-têtes et du corps (multipart). |
| `OdooConnector` | — | Accès XML-RPC à Odoo (`get`, `put`, …). |

### Utilitaires transverses

Aucun extra requis : ces symboles fonctionnent avec l'installation de base.

| Symbole | Rôle |
|---|---|
| `Logger` | Logger unifié console / fichier / base, avec niveaux, contexte `disabled()` et insertion dans une table de détails. |
| `timer`, `get_timer`, `set_timer`, `now`, `set_timezone` | Décorateur de chronométrage et helpers de temps (timezone-aware). |
| `retry_on_failure` | Décorateur de retry avec backoff et méthode de reconnexion optionnelle. |
| `get_public_ip` | Récupération de l'IP publique (utile pour pare-feux). |
| `Env` | Chargement d'un `.env` trouvé automatiquement dans l'arborescence parente. |

## Installation

### Utilisation du package (public)

Le package est publié sur PyPI et installable par n'importe qui :

```bash
pip install adstoolbox
```

Cette installation de base est volontairement légère (environ 20 Mo) et couvre `Logger`,
`Env`, `timer`, `MailReader` et `OdooConnector`. **Les autres modules demandent un extra**,
à choisir selon les besoins :

```bash
pip install "adstoolbox[pgsql]"              # PostgreSQL + Polars
pip install "adstoolbox[mssql,mysql,pgsql]"  # les trois bases
pip install "adstoolbox[files]"              # FileHandler (fsspec + backends)
pip install "adstoolbox[all]"                # tout (environ 480 Mo)
```

Les guillemets sont nécessaires sous `zsh`, qui interprète les crochets.

> **Migration depuis les versions ≤ 2026.05.19**
> Ces versions installaient toutes les dépendances d'office. Depuis, elles sont
> optionnelles. Pour retrouver le comportement précédent en une commande :
> `pip install "adstoolbox[all]"`. Les imports étant paresseux, une dépendance
> manquante ne casse plus `import adsToolBox` : l'erreur survient à l'utilisation
> du module concerné, et nomme l'extra à installer.

### Développement (interne ADS uniquement)

L'accès aux sources est restreint aux employés d'Alchimie Data Solutions. Une fois le
dépôt cloné via les accès internes, le projet est géré avec **Poetry**
(voir `pyproject.toml` et `poetry.lock`) :

```bash
poetry install --all-extras
```

`--all-extras` est indispensable : sans lui, ni Polars, ni `fsspec`, ni les drivers SQL ne
sont installés, et la suite de tests échoue dès la collecte. Le groupe `dev`
(`pytest`, `testcontainers`, `ruff`) est inclus par défaut.

## Modules et extras

Les modules sont chargés **paresseusement** (PEP 562) : `import adsToolBox` n'importe
aucune dépendance tierce, chaque module n'est chargé qu'au premier accès à l'un de ses
symboles. Une dépendance absente produit un message explicite :

```
ImportError: DbPgsql requiert une dépendance non installée (No module named 'psycopg2').
Installez-la avec : pip install "adstoolbox[pgsql]".
```

| Extra | Paquets installés | Modules débloqués |
|---|---|---|
| *(aucun)* | `requests`, `chardet`, `python-dotenv`, `tzdata` (Windows) | `logger`, `timer`, `global_config`, `load_env`, `mail_reader`, `odoo`, `dml_generator` |
| `dataframe` | `polars` | `data_factory`, `pipeline`, `data_comparator` |
| `mssql` | `polars`, `pymssql` | `db_mssql` |
| `mysql` | `polars`, `pymysql` | `db_mysql` |
| `pgsql` | `polars`, `psycopg2-binary` | `db_pgsql` |
| `files` | `fsspec`, `adlfs`, `smbprotocol`, `paramiko` | `file_handler` |
| `git` | `GitPython`, `PyGithub` | `git_handler` |
| `google` | `polars`, `google-api-python-client`, `google-auth`, `google-auth-oauthlib` | `google_calendar` |
| `cdc` | `SQLAlchemy`, `jsonschema` | `cdc`, `ddl_operations` |
| `all` | tous les précédents | tous |

Les extras des bases incluent `polars` parce que `db_*` en dépend via `data_factory` :
`adstoolbox[pgsql]` est donc autosuffisant.

### Note sur Polars et les anciens processeurs

Le package dépend de `polars`, le build standard (AVX2). Sur un processeur sans AVX2, il
faut basculer sur `polars-lts-cpu` **après** l'installation :

```bash
pip install "adstoolbox[pgsql]"
pip install --force-reinstall --no-deps polars-lts-cpu
```

`--force-reinstall` est nécessaire : les deux distributions fournissent le même module
`polars` sans se déclarer incompatibles, et `pip` considère la contrainte satisfaite sans
remplacer les fichiers. Ne déclarez jamais les deux dans un même fichier de dépendances —
elles s'écrasent mutuellement et rendent `import polars` inutilisable.

## Démarrage rapide

### Connexion à une base de données

Nécessite `pip install "adstoolbox[pgsql]"`.

```python
from adsToolBox import DbPgsql, Logger, Env

logger = Logger(log_level=Logger.INFO, logger_name="adsLogger")
env = Env(logger)

db = DbPgsql({
    'database': env.PG_DWH_DB,
    'user': env.PG_DWH_USER,
    'password': env.PG_DWH_PWD,
    'port': env.PG_DWH_PORT,
    'host': env.PG_DWH_HOST
}, logger)
db.connect()

generator = db.sql_query("SELECT * FROM table_test;")

for batch in generator:
    for row in batch:
        data = row
```

### Pipeline source → destination

Nécessite `pip install "adstoolbox[dataframe]"`, plus l'extra de chaque base utilisée.

```python
from adsToolBox import Pipeline

pipeline = Pipeline(
    {
        "db_source": db_src,
        "query_source": "SELECT * FROM source_table",
        "db_destination": {
            "name": "demo",
            "db": db_dst,
            "table": "destination_table",
            "cols": ["col1", "col2"],
            "cols_def": ["INT", "VARCHAR(50)"],
        },
        "operation_type": "insert",
        "insert_method": "bulk",
        "batch_size": 10_000,
    },
    logger,
)
results = pipeline.run()

print(results)
```

### Logger et chronomètre

Aucun extra nécessaire.

```python
from adsToolBox import Logger, set_timer, timer

set_timer(state=True)

class MyJob:
    def __init__(self) -> None:
        self.logger = Logger(log_level=Logger.DEBUG)

    @timer
    def run(self) -> None:
        self.logger.info("traitement en cours")
```

D'autres exemples sont disponibles dans le dépôt de démo :
[AlchimieDataSolutions/DemoPy](https://github.com/AlchimieDataSolutions/DemoPy).

## Structure du dépôt

```
adsGenericFunctions/
├── .github/workflows/       # CI : tests, matrice d'extras, contrôle du paquet publié
├── adsToolBox/              # Package publié
│   ├── __init__.py          # Exports publics et chargement paresseux (PEP 562)
│   ├── cdc.py               # ChangeDataCapture + modes SCD
│   ├── data_comparator.py   # DataComparator
│   ├── data_factory.py      # DataFactory (classe abstraite)
│   ├── db_mssql.py          # DbMssql
│   ├── db_mysql.py          # DbMysql
│   ├── db_pgsql.py          # DbPgsql
│   ├── ddl_operations.py    # Génération DDL multi-dialecte
│   ├── dml_generator.py     # Génération DML multi-dialecte
│   ├── file_handler.py      # FileHandler (fsspec)
│   ├── git_handler.py       # GitHandler
│   ├── global_config.py     # retry_on_failure, set_timer, get_public_ip
│   ├── google_calendar.py   # GoogleCalendarConnector
│   ├── load_env.py          # Env
│   ├── logger.py            # Logger
│   ├── mail_reader.py       # MailReader
│   ├── odoo.py              # OdooConnector
│   ├── pipeline.py          # Pipeline
│   └── timer.py             # timer, now, set_timezone, get_timer
├── scripts/
│   └── check_extras.py      # Valide le contrat des extras (utilisé par la CI)
├── tests/                   # Tests unitaires (mocks)
├── integration_tests/       # Tests fonctionnels (testcontainers → Docker)
├── adsGenericFunctions.py   # Point d'entrée historique
├── pyproject.toml           # Métadonnées PEP 621 + configuration Ruff
├── poetry.lock              # Versions figées (source de vérité pour la CI)
└── pytest.ini               # Configuration pytest
```

## Tests

Les tests sont répartis en deux suites, déclarées dans `pytest.ini` :

- **`tests/`** — tests unitaires avec mocks (`unittest.mock`), sans dépendance externe.
- **`integration_tests/`** — tests fonctionnels avec **[Testcontainers](https://testcontainers.com/)**
  (SQL Server, MySQL, PostgreSQL, Samba, Azurite) ; **Docker doit être disponible**.

Les deux suites supposent une installation `--all-extras` : plusieurs fichiers de test
importent `polars` et `fsspec` directement, donc une installation partielle échoue à la
collecte.

### Exécuter toutes les suites

```bash
poetry run pytest
```

### Cibler une suite

```bash
poetry run pytest tests/                 # unitaires uniquement
poetry run pytest integration_tests/     # fonctionnels uniquement
poetry run pytest tests/test_logger.py   # un fichier précis
```

### Tests de garde sur le chargement paresseux

`tests/test_init_lazy.py` protège trois propriétés faciles à casser en silence :

- `import adsToolBox` ne charge aucune dépendance tierce ;
- `timer` reste la fonction et n'est pas masqué par le sous-module homonyme ;
- aucun symbole exporté n'est masqué par un sous-module de même nom.

Ces tests s'ignorent d'eux-mêmes pour les cas dont l'extra est absent, et sont donc
exécutables sur une installation partielle.

### Contrat des extras

```bash
python scripts/check_extras.py            # aucun extra installé
python scripts/check_extras.py pgsql      # extras installés
python scripts/check_extras.py all
```

Le script vérifie que le cœur reste importable et que chaque symbole indisponible nomme un
extra qui n'est effectivement pas installé. La CI l'exécute sur une matrice de
configurations : c'est le seul endroit capable de détecter une régression de couplage, le
job principal installant tout.

### Tests fonctionnels — prérequis

- Docker Desktop (ou équivalent) lancé et accessible.
- Les containers sont démarrés automatiquement par les fixtures (pas de setup manuel).
- Sous Windows, la variable `TESTCONTAINERS_RYUK_DISABLED=true` est positionnée par les
  tests pour éviter les problèmes de cleanup.

## Développement

### Linter / formatter

Le projet utilise **Ruff** (configuration dans `pyproject.toml`, `select = ["ALL"]` avec
quelques exceptions documentées) :

```bash
poetry run ruff check .
poetry run ruff format .
```

Cibles configurées : `adsToolBox`, `tests`, `integration_tests`, `scripts`.
Longueur de ligne : 100.

### Conventions

- Python ≥ 3.10, typage explicite et `from __future__ import annotations` quand utile.
- Docstrings en français, style concis.
- Noms en `snake_case` pour les méthodes publiques.
- Tests unitaires obligatoires pour toute nouvelle méthode publique.

### Ajouter une dépendance

Toute nouvelle dépendance tierce doit être **optionnelle** et rattachée à un extra, sauf
si elle est pure Python et de taille négligeable. Le critère : un paquet binaire (donc
susceptible d'échouer à l'installation), lourd, ou exigeant un binaire système va en extra.

Trois endroits à mettre à jour de façon cohérente :

1. `[project.optional-dependencies]` dans `pyproject.toml`, extra dédié **et** liste `all` ;
2. `_EXTRA_OF_MODULE` dans `adsToolBox/__init__.py`, pour le message d'erreur ;
3. la matrice du workflow CI, si l'extra est nouveau.

Le module concerné doit importer sa dépendance au niveau module (jamais depuis
`__init__.py`), afin que le chargement paresseux isole la panne.

### Publication

Le package est publié sur PyPI sous le nom `adstoolbox`. La version suit un schéma
calendaire `YYYY.MM.DD` défini dans `pyproject.toml`.

```bash
poetry build
poetry publish
```

## Dépendances

Les dépendances sont déclarées dans `pyproject.toml`, sections `[project.dependencies]`
(cœur) et `[project.optional-dependencies]` (extras). Les versions sont exprimées en
bornes ouvertes ; `poetry.lock` fixe les versions exactes pour la CI.

### Cœur — toujours installé

Pur Python, taille négligeable, aucun risque d'échec d'installation.

- `requests`, `chardet` — `global_config`, `mail_reader`
- `python-dotenv` — `Env`
- `tzdata` — base IANA pour `zoneinfo` (Windows uniquement, via marqueur d'environnement)

### Extras

- **Données** : `polars`
- **Bases de données** : `pymssql` (MSSQL), `pymysql` (MySQL), `psycopg2-binary` (PostgreSQL)
- **CDC** : `SQLAlchemy`, `jsonschema`
- **Fichiers** : `fsspec`, plus les backends qu'il charge dynamiquement — `adlfs`
  (Azure `abfs://`), `paramiko` (SFTP `sftp://`), `smbprotocol` (SMB `smb://`). Ces trois
  paquets ne sont jamais importés directement mais sont requis à l'exécution.
- **Intégrations** : `GitPython`, `PyGithub`, `google-api-python-client`, `google-auth`,
  `google-auth-oauthlib`

### Développement

Groupe `dev` du `pyproject.toml`, jamais installé chez les utilisateurs :
`pytest`, `testcontainers`, `ruff`.

## Auteurs

- Olivier Siguré — <olivier.sigure@alchimiedatasolutions.com>
- Matthieu Vannin — <matthieu.vannin@alchimiedatasolutions.com>
- Antoine Ducoulombier — <antoine.ducoulombier@alchimiedatasolutions.com>
- Pierre Baux — <pierre.baux@alchimiedatasolutions.com>

## Licence

Distribué sous licence **MIT**.

