Metadata-Version: 2.5
Name: noria
Version: 0.1.2
Summary: SQL-first data pipeline framework — Jinja2 templating, multi-database, built-in monitoring
Author: Noria Contributors
License: MIT
License-File: LICENSE
Keywords: data,duckdb,etl,jinja2,orchestration,pipeline,sql
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Database
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Requires-Dist: duckdb-engine>=0.13.0
Requires-Dist: duckdb>=0.10.0
Requires-Dist: fastapi>=0.110.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: jinja2>=3.1.0
Requires-Dist: pydantic>=2.5.0
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: pytz>=2024.1
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.7.0
Requires-Dist: sqlalchemy>=2.0.0
Requires-Dist: typer>=0.12.0
Requires-Dist: uvicorn[standard]>=0.29.0
Provides-Extra: all
Requires-Dist: google-cloud-bigquery>=3.0.0; extra == 'all'
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'all'
Requires-Dist: snowflake-sqlalchemy>=1.5.0; extra == 'all'
Requires-Dist: sqlalchemy-bigquery>=1.9.0; extra == 'all'
Requires-Dist: watchdog>=4.0.0; extra == 'all'
Provides-Extra: bigquery
Requires-Dist: google-cloud-bigquery>=3.0.0; extra == 'bigquery'
Requires-Dist: sqlalchemy-bigquery>=1.9.0; extra == 'bigquery'
Provides-Extra: dev
Requires-Dist: mypy>=1.9.0; extra == 'dev'
Requires-Dist: pre-commit>=3.7.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Requires-Dist: watchdog>=4.0.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
Requires-Dist: mkdocs>=1.5.0; extra == 'docs'
Provides-Extra: postgres
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'postgres'
Provides-Extra: snowflake
Requires-Dist: snowflake-sqlalchemy>=1.5.0; extra == 'snowflake'
Provides-Extra: watch
Requires-Dist: watchdog>=4.0.0; extra == 'watch'
Description-Content-Type: text/markdown

<p align="center">
  <img src="docs/assets/logo.svg" alt="Noria" width="220"/>
</p>

<p align="center">
  <strong>SQL-first data pipeline framework</strong><br/>
  Jinja2 templating · multi-base de données · monitoring intégré
</p>

<p align="center">
  <a href="https://pypi.org/project/noria/"><img src="https://img.shields.io/pypi/v/noria?color=0ea5e9&label=PyPI" alt="PyPI"/></a>
  <a href="https://pypi.org/project/noria/"><img src="https://img.shields.io/pypi/pyversions/noria?color=06b6d4" alt="Python"/></a>
  <img src="https://img.shields.io/badge/license-MIT-38bdf8" alt="MIT"/>
</p>

---

> *"Noria"* — la roue hydraulique qui fait circuler l'eau avec précision et régularité.  
> Ici, vos données circulent avec la même fiabilité.

---

## Pourquoi Noria ?

| Besoin | Solution Noria |
|--------|---------------|
| Écrire du SQL, pas du Python ORM | Templates Jinja2 directement dans vos fichiers `.sql` |
| Changer de BDD sans tout réécrire | Macros cross-DB compilées selon le dialecte cible |
| Orchestrer des dépendances entre requêtes | DAG déclaratif en YAML, exécution parallèle |
| Voir ce qui se passe | Dashboard de monitoring intégré, historique SQLite |
| Démarrer en 2 minutes | `pip install noria && noria init mon_projet && noria run` |

---

## Installation

Noria s'installe dans un **environnement Python** : un dossier de travail qui contiendra Noria et vos projets.

```bash
# Installer uv (une seule fois sur votre machine)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Créer votre espace de travail et y installer Noria
mkdir data_projets && cd data_projets
uv venv                          # crée l'environnement Python (.venv)
source .venv/bin/activate        # l'active (à refaire à chaque nouveau terminal)
uv pip install noria

noria version                    # vérification : affiche la version installée
```

> **Sans uv** : `python3 -m venv .venv && source .venv/bin/activate && pip install noria` fait exactement la même chose.

> **« noria : commande introuvable » ?** L'environnement n'est pas activé : relancez `source .venv/bin/activate` depuis le dossier `data_projets`.

---

## Démarrage rapide

```bash
noria init mon_pipeline          # Noria pose quelques questions (base de données…)
                                 # Entrée pour garder les valeurs par défaut
cd mon_pipeline
noria run                        # exécute le pipeline d'exemple
```

Organisation générée :

```
data_projets/            ← espace de travail (environnement Python)
├── .venv/               ← Noria installé ici
└── mon_pipeline/        ← votre projet Noria
    ├── noria.yml
    ├── envs/
    └── sql/
```

Pour un deuxième projet : revenez dans `data_projets`, puis `noria init autre_projet`.

---

## Structure d'un projet Noria

```
mon_pipeline/
├── noria.yml                  # Configuration du pipeline
├── sql/
│   ├── macros/                # Macros Jinja2 réutilisables
│   │   └── utils.sql.j2
│   ├── 01_clients.sql         # Requêtes SQL avec templates Jinja2
│   └── 02_rapport.sql
├── params/
│   └── prod.yml               # Paramètres par environnement
└── .noria/
    ├── data.db                # Base DuckDB locale (développement)
    └── tracking.db            # Historique des exécutions
```

---

## Configuration — `noria.yml`

```yaml
name: mon_pipeline
description: "Pipeline de données clients"

database:
  dialect: duckdb          # duckdb | postgresql
  database: .noria/data.db

params:
  date_min: "2024-01-01"
  schema: main

monitoring:
  enabled: true
  port: 7070

tasks:
  clients_actifs:
    sql: sql/01_clients.sql
    target: "{{ params.schema }}.clients_actifs"
    write_mode: replace        # replace | append | truncate | merge
    active: true
    description: "Clients avec au moins une commande"

  rapport:
    sql: sql/02_rapport.sql
    target: "{{ params.schema }}.rapport"
    write_mode: replace
    depends_on:
      - clients_actifs          # exécuté après clients_actifs
    pre_hook:
      file: sql/hooks/pre_rapport.sql
    post_hook:
      file: sql/hooks/post_rapport.sql
```

---

## Écrire du SQL avec Jinja2

```sql
-- sql/01_clients.sql

SELECT
    {{ cast('id', 'VARCHAR') }}             AS id_client,  -- adapté au dialecte
    first_name || ' ' || last_name          AS nom,
    {{ date_trunc('month', 'created_at') }} AS mois_inscription,
    {{ coalesce('phone', "'N/A'") }}        AS telephone

FROM source_schema.customers
WHERE created_at >= '{{ params.date_min }}'

{% if params.actif_only %}
  AND actif = TRUE
{% endif %}
```

### Macros cross-base de données

| Macro Jinja2 | PostgreSQL compilé | DuckDB compilé |
|---|---|---|
| `{{ cast('id', 'TEXT') }}` | `id::TEXT` | `CAST(id AS TEXT)` |
| `{{ date_trunc('month', 'ts') }}` | `DATE_TRUNC('month', ts)` | `DATE_TRUNC('month', ts)` |
| `{{ current_timestamp() }}` | `NOW()` | `CURRENT_TIMESTAMP` |
| `{{ concat('a', 'b') }}` | `CONCAT(a, b)` | `CONCAT(a, b)` |
| `{{ coalesce('a', '0') }}` | `COALESCE(a, 0)` | `COALESCE(a, 0)` |
| `{{ safe_divide('num', 'den') }}` | `CASE WHEN den=0 THEN NULL...` | `CASE WHEN den=0 THEN NULL...` |
| `{{ to_char('ts', 'YYYY-MM') }}` | `TO_CHAR(ts, 'YYYY-MM')` | `STRFTIME(ts, 'YYYY-MM')` |
| `{{ limit(10) }}` | `LIMIT 10` | `LIMIT 10` |
| `{{ schema_table('s', 't') }}` | `s.t` | `s.t` |

### Macros personnalisées

Créez vos propres macros dans `sql/macros/*.sql.j2` :

```jinja
{# sql/macros/utils.sql.j2 #}
{% macro panier_moyen(montant, quantite) %}
  {{ safe_divide(montant, quantite) }}
{% endmacro %}
```

Puis utilisez-les dans n'importe quel fichier SQL :

```sql
{% from 'utils.sql.j2' import panier_moyen %}
SELECT {{ panier_moyen('SUM(amount)', 'COUNT(id)') }} AS panier_moyen
FROM orders
```

---

## Modes d'écriture (`write_mode`)

| Mode | Comportement |
|------|-------------|
| `replace` | DROP TABLE + CREATE TABLE AS SELECT |
| `append` | INSERT INTO ... SELECT |
| `truncate` | TRUNCATE + INSERT |
| `merge` | INSERT ON CONFLICT (nécessite `merge_key`) |
| `create_if_not_exists` | Crée seulement si absente |

---

## CLI — Commandes

```bash
noria init <nom>          # Initialiser un projet
noria validate            # Valider config + DAG (sans connexion BDD)
noria compile             # Prévisualiser le SQL compilé
noria compile -t ma_tache # Compiler une seule tâche
noria run                 # Lancer tout le pipeline
noria run -t tache_a      # Lancer une tâche spécifique
noria run --dry-run       # Compiler sans exécuter
noria run --workers 8     # Parallélisme (défaut: 4)
noria run --param date_min=2024-06-01  # Surcharger un paramètre
noria tasks               # Lister les tâches et dépendances
noria status              # Voir l'historique des exécutions
noria monitor             # Ouvrir le dashboard web
```

---

## Paramétrage par environnement

```bash
# Développement (défaut)
noria run

# Production via surcharge CLI
noria run --param schema=prod --param date_min=2023-01-01

# Variables d'environnement dans noria.yml
# database.password: ${DB_PASSWORD}
```

---

## Hooks pre/post

```yaml
tasks:
  ma_tache:
    sql: sql/ma_tache.sql
    pre_hook:
      file: sql/hooks/pre.sql     # Exécuté avant la tâche
      ignore_on_error: false
    post_hook:
      file: sql/hooks/post.sql    # Exécuté après
      ignore_on_error: true       # Erreur non bloquante
```

---

## Monitoring

Le dashboard web est inclus par défaut :

```bash
noria monitor             # Ouvre http://localhost:7070
```

- Historique de toutes les exécutions
- Détail par tâche : statut, durée, lignes écrites, erreurs
- Rafraîchissement automatique (HTMX)

L'historique est stocké localement dans `.noria/tracking.db` (SQLite, zéro config).

---

## API Python

```python
from noria.core.pipeline import Pipeline

# Lancer le pipeline
pipeline = Pipeline("./mon_projet", params_override={"schema": "staging"})
results = pipeline.run()

# Compilation uniquement
compiled_sql = pipeline.compile(task_name="ma_tache")
print(compiled_sql["ma_tache"])

# Historique
history = pipeline.history(limit=5)
```

---

## Ajouter un dialecte personnalisé

```python
from noria.adapters import register_adapter
from noria.adapters.base import BaseAdapter
from sqlalchemy import create_engine

class MyAdapter(BaseAdapter):
    dialect_name = "mydb"
    
    def create_engine(self, db_config):
        return create_engine(f"mydb://...")
    
    # ... implémenter les méthodes abstraites

register_adapter("mydb", MyAdapter)
```

---

## Tests

```bash
make install-dev
make test
make test-cov   # avec rapport de couverture HTML
```

---

## Contribuer

1. Fork le dépôt
2. `make install-dev`
3. Créer une branche feature
4. `make test` + `make lint` avant de soumettre

---

## Licence

MIT © Noria Contributors
