Design — pinky-provider
Update date : 2026-06-02 06:52
Vision
Provider Python léger et YAML-driven pour déployer des objets Snowflake de manière déclarative et idempotente. Alternative à Terraform pour les équipes data qui veulent rester dans l'écosystème Python/YAML sans dépendance Go/HCL.
Statut d'implémentation
Première tranche livrée : le moteur + le type
warehouse(account scope), bout-en-bout. - Moteur :resources/base.py(_sdk_fields()+ injection dynamique des champs SDK, ADR-0001),core/manifest.py(loader YAML + Jinjavars/<ENV>.yml, scope),core/diff.py(compute_diff),core/session.py(query tag ADR-0011 +get_session),AccountProvider+ registry,Provider, CLIplan/apply(rendu rich, ADR-0003). - DDL warehouse via Core APIcreate_or_alter— pas de SQL fallback. - Tests : 31 unitaires verts (dont le binding réelsnowflake.core.warehouse.Warehouse). ruff + mypy strict OK. - Validé en apply réel sur un compte SANDBOX : CREATE → idempotent UNCHANGED → ALTER (size) → idempotent. - Couche de curation (ADR-0015) : les warts SDK trouvés en apply réel sont nettoyés —auto_resumeexposé enbool(cast →"true"),warehouse_sizeen enum soupleWarehouseSize | str(autocomplété mais une taille inconnue/future passe — validée par Snowflake à l'apply, pas de lock-in), diff normalisé pour les formes display (X-Small,2X-Large). Le YAML reste idiomatique. - JSON Schema généré (pinky-provider schemas) depuis les modèles curés + wiringyaml.schemas→ autocomplétion champs/valeurs enum, validation types + champs inconnus dans l'éditeur, avant tout apply.Tous les autres types des tables ci-dessous restent déclarés/planifiés (stubs). Le DAG, le mode Native App (
build_setup_script),_depends_on(topo-sort) et les objets SQL fallback (EAI, storage_lifecycle_policy) ne sont pas encore implémentés.
Principes
- Déclaratif : chaque objet Snowflake = 1 fichier
.ymldécrivant l'état souhaité - Idempotent : le provider applique la bonne DDL selon ce qui existe (create_or_alter / IF NOT EXISTS / OR REPLACE)
- Typé : chaque resource est un
BaseModelPydantic v2 qui valide les champs via_sdk_fields()introspection du SDK — zéro mapping manuel, zéro maintenance quand Snowflake ajoute des champs - Zéro dépendance infra : pas de state file, pas de backend, pas de lock. L'état c'est Snowflake lui-même.
- CI-agnostic : fonctionne avec GitLab, GitHub Actions, ou en local. La CI n'est qu'un trigger.
- Edition-aware : le provider détecte Standard / Enterprise / Business Critical au runtime et adapte la DDL — les fonctionnalités non disponibles génèrent un warning explicite, pas une erreur.
- 1 schema = 1 chose : pinky est l'extension du modèle Snowflake lui-même — pas une convention arbitraire.
Snowflake applique ce principe sur sa propre plateforme (
Account= 1 client,Database= 1 domaine,Schema= 1 unité de travail,Object= 1 chose précise). pinky applique le même modèle fractal un niveau au-dessus. Un schema est l'unité atomique de : - déploiement — 1 schema = 1 repo Git = 1 pipeline CI/CD
- cycle de vie —
CREATE SCHEMA= naissance,DROP SCHEMA= sortie propre et totale - responsabilité — 1 périmètre fonctionnel clair, sans ambiguïté
Tout ce qui appartient à un processus vit dans son schema.
DROP SCHEMAest l'opération d'offboarding atomique : rien n'est orphelin, rien ne nécessite de nettoyage manuel.
Couverture des objets Snowflake
Arbre de décision DDL
create_or_alter disponible en Core API → CORE API
CREATE OR ALTER disponible en SQL → SQL
stateful (IF NOT EXISTS) → CORE API
stateless (OR REPLACE) → CORE API
Core API absente → SQL (fallback temporaire)
Objets account-level
⚠️ = objet stocké physiquement dans un schéma, accessible au niveau account.
| Object | Edition | v1 | v2 | later | out | API |
|---|---|---|---|---|---|---|
| api_integration | ✓ | CORE API | ||||
| catalog_integration (Iceberg) | ✓ | CORE API | ||||
| compute_pool (SPCS) | E | ✓ | CORE API | |||
| database_role | ✓ | CORE API | ||||
| external_volume (Iceberg) | ✓ | CORE API | ||||
| grant | ✓ | CORE API | ||||
| image_repository (SPCS) | E | ✓ | CORE API | |||
| managed_account | ✓ | CORE API | ||||
| masking_policy ⚠️ | E | ✓ | SQL | |||
| network_policy | ✓ | CORE API | ||||
| network_rule ⚠️ | ✓ | CORE API | ||||
| notification_integration | ✓ | CORE API | ||||
| password_policy ⚠️ | ✓ | CORE API | ||||
| role | ✓ | CORE API | ||||
| row_access_policy ⚠️ | E | ✓ | SQL | |||
| secret ⚠️ | ✓ | CORE API | ||||
| user (delegated to IdP — TBD) | ? | CORE API | ||||
| warehouse | ✓ | CORE API |
Objets schema-level
| Object | Edition | v1 | v2 | later | out | API |
|---|---|---|---|---|---|---|
| alert | ✓ | CORE API | ||||
| artifact_repository (SPCS) | E | ✓ | CORE API | |||
| dynamic_table | ✓ | CORE API | ||||
| event_table | ✓ | CORE API | ||||
| external_access_integration | ✓ | SQL | ||||
| function (External Functions only) | ✓ | CORE API | ||||
| iceberg_table | ✓ | CORE API | ||||
| notebook | ✓ | CORE API | ||||
| pipe | ✓ | CORE API | ||||
| procedure | ✓ | CORE API | ||||
| sequence | ✓ | CORE API | ||||
| service (SPCS) | E | ✓ | CORE API | |||
| stage | ✓ | CORE API | ||||
| storage_lifecycle_policy | ✓ | SQL | ||||
| stream | ✓ | CORE API | ||||
| streamlit | ✓ | CORE API | ||||
| table | ✓ | CORE API | ||||
| tag | ✓ | CORE API | ||||
| task (standalone) | ✓ | CORE API | ||||
| task.dagv1 (DAG — separate module) | ✓ | → dag.md | ||||
| user_defined_function | ✓ | CORE API | ||||
| view | ✓ | CORE API |
DDL par type de resource
| Type | Chemin principal | Fallback | Notes |
|---|---|---|---|
| Database | IF NOT EXISTS |
— | |
| Schema | IF NOT EXISTS |
— | ALTER SQL post-create pour _log_level/_trace_level — gap permanent Core API |
| Event Table | IF NOT EXISTS |
— | _storage_lifecycle_policy = SQL post-create |
| Tag | IF NOT EXISTS |
— | appliqué via ALTER … SET TAG |
| Table | IF NOT EXISTS + diff colonnes |
— | → ADR-0007 |
| Stream | IF NOT EXISTS |
— | stateful — jamais OR REPLACE (→ ADR-0006) |
| Warehouse | create_or_alter() |
or_replace + copy_grants |
|
| Stage | create_or_alter() |
or_replace + copy_grants |
|
| Procedure | create_or_alter() |
or_replace + copy_grants |
handler changé → exception → or_replace |
| Function (UDF) | or_replace + copy_grants |
GET_DDL bootstrap | pas de create_or_alter sur la collection |
| View | create_or_alter() |
or_replace + copy_grants |
column comments → SQL post-create non-bloquant |
| Dynamic Table | create_or_alter() |
or_replace + copy_grants |
columns → pop avant from_dict() |
| Task | create_or_alter() |
or_replace + copy_grants |
serverless uniquement |
| Secret | or_replace |
— | create_or_alter absent sur SecretCollection |
| Network Rule | or_replace |
— | idem |
| EAI | SQL fallback | — | absent de la Core API |
| Storage Lifecycle Policy | SQL fallback | — | absent de la Core API |
| Semantic View | SQL fallback | — | CREATE OR REPLACE SEMANTIC VIEW — Core API absente |
| DAG | meta-resource | — | N Tasks + wrappers auto-injectés → explanation/dag.md |
_to_ddl() existe uniquement quand la Core API ne couvre pas encore la stratégie souhaitée.
Commenté comme dette temporaire. Supprimé dès que create_or_alter disponible.
Manifest et scope
Les 3 niveaux de scope
| Scope | Zone d'autorité | DROP auto |
|---|---|---|
schema |
1 schema | dans le schema uniquement |
database |
1 database | non |
account |
account entier | jamais |
Variabilisation par env
manifest.yml
vars/
SANDBOX.yml
PRODUCTION.yml
{{vars.key}} disponible dans toute valeur YAML. Clé absente → erreur explicite, pas de fallback silencieux.
EnvPattern — résolution depuis la session Snowflake
DATABASE_SUFFIX # MY_DB_SANDBOX → SANDBOX (défaut)
DATABASE_PREFIX # SANDBOX_MY_DB → SANDBOX
SCHEMA_SUFFIX # MY_SCHEMA_QA → QA
SCHEMA_PREFIX # QA_MY_SCHEMA → QA
ACCOUNT # convention multi-account
Dépendances entre objets
_depends_on:
- network_rule/workday
- secret/vendor_oauth
Résolution via graphlib.TopologicalSorter — ordre de déploiement garanti, cycle détecté à l'init.
Scope
Le provider distingue deux catégories d'objets, alignées sur la structure de la Core API :
- Infra account-level — objets de configuration qui opèrent au niveau du compte. Certains sont stockés physiquement dans un schéma (network_rule, secret…) mais restent référençables cross-schéma.
- Data schema-level — objets de pipeline rattachés à un schéma : tables, vues, procédures, tâches…
Les classes de resource sont agnostiques du scope — manifest.yml assemble
les deux catégories pour un déploiement cohérent.
Notifications
Deux canaux, détection automatique du mode :
| Canal | Déclencheur | Destinataires |
|---|---|---|
alerts_to |
Erreur technique pipeline | Équipe technique (Teams/Slack webhook) |
contact_to |
Rejet métier dans les données | Équipe métier (email DL / URL Teams) |
def detect_notification_mode(manifest) -> Literal["alert", "finalize"]:
return "alert" if manifest.log_level is not None else "finalize"
- Snowflake Alerts (si
log_levelconfiguré) : requête surSNOWFLAKE.TELEMETRY.EVENTS - FINALIZE_TASK (sinon) : SP injectée en fin de DAG
Types supportés : email | slack | teams | custom_sp
Contacts résolus dynamiquement via SNOWFLAKE.CORE.GET_CONTACTS() — pas d'email hardcodé.
RBAC — Database roles
4 suffixes standardisés, calqués sur la sémantique native Snowflake :
| Suffix | Sémantique | Privileges |
|---|---|---|
_VIEWER |
Observe | SELECT, USAGE, MONITOR |
_USER |
Utilise | + OPERATE tasks |
_CREATOR |
Produit des données | + INSERT/UPDATE/DELETE |
_ADMIN |
Produit des objets | + CREATE TABLE/VIEW/TASK/etc. |
Hiérarchie : ADMIN > CREATOR > USER > VIEWER. 2 niveaux : full (IN DATABASE) + scoped (IN SCHEMA).
Les account roles héritent des database roles — zéro grants directs, zéro couplage entre repos.
RACI
| Quoi | Responsable | Outil |
|---|---|---|
| Qualité code | Pre-commit + CI gate | ruff, py_compile |
| Conformité YAML | Pre-commit + CI gate | Pydantic + hooks custom |
| Conventions métier | Pre-commit + CI gate | hooks custom |
| Ordre de déploiement | Provider | graphlib.TopologicalSorter |
| Déployer dans Snowflake | Provider | snowflake.core |
Le provider fait confiance aux gates en amont. Il ne valide pas, il déploie.
CLI
- click — commande = fonction + décorateurs, intégration native rich, complétion shell
- rich / rich-click — rendu
plan/applyavec statuts colorés, spinner par objet, fallback texte plat en CI - Pinky Cash Back — dès qu'un outil détecte une sous-optimisation de crédits, il la signale
proactivement avec un ton direct et léger :
🤑 save credits — reorder your yml. Le principe s'applique partout : YML non normalisé, warehouse sur task serverless, DROP+CREATE évitable par un ALTER… Un outil qu'on utilise seul toute la journée a le droit d'être drôle.
Référence commandes → reference/cli.md
Distribution
- PyPI :
pip install pinky-provider— point d'entrée universel - GitHub : dépôt privé
- GitHub Actions : CI/CD du provider lui-même
- GitHub Pages : documentation