Skip to content

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 + Jinja vars/<ENV>.yml, scope), core/diff.py (compute_diff), core/session.py (query tag ADR-0011 + get_session), AccountProvider + registry, Provider, CLI plan/apply (rendu rich, ADR-0003). - DDL warehouse via Core API create_or_alter — pas de SQL fallback. - Tests : 31 unitaires verts (dont le binding réel snowflake.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_resume exposé en bool (cast → "true"), warehouse_size en enum souple WarehouseSize | 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 + wiring yaml.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 .yml dé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 BaseModel Pydantic 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 vieCREATE 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 SCHEMA est 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 columnspop 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_level configuré) : requête sur SNOWFLAKE.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/apply avec 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