Aller au contenu

Référence de syntaxe

Cette page est la référence canonique de mecapy.yml v1 : chaque section décrit un bloc du manifest, son type, ses valeurs par défaut et ses contraintes.

version: "1"

Figé à "1". Toute autre valeur est rejetée. Un futur format v2 conservera la même clé en changeant le littéral — vos manifests existants restent valides tant que MecaPy maintient le parseur v1.

package:
name: bolt-sizing # requis
version: 0.1.0 # requis, semver
description: | # optionnel
Résumé en une ligne ; multi-ligne autorisé.
author: Jane Doe # optionnel
license: MIT # optionnel
visibility: private # optionnel, défaut "private"
organization: acme-corp # requis si visibility=internal
tags: [bolts, mechanics] # optionnel
engine: # optionnel — uniquement pour les wrappers
name: code-aster
version: "17.4.0"
resources: # optionnel, voir plus bas
recommended: { cpu: 1, memory_mb: 512 }
timeout: 60
ChampTypeDéfautNotes
namechaîneIdentifiant du package. Kebab-case recommandé.
versionchaîneSemver strict : MAJEUR.MINEUR.PATCH (tag pre-release optionnel). C’est la version du wrapper, pas celle de l’engine.
descriptionchaînenullTexte libre.
authorchaînenullTexte libre.
licensechaînenullIdentifiant SPDX (MIT, Apache-2.0, …).
visibilityenum"private"private / internal / public. Voir visibilité.
organizationchaînenullSlug de l’organisation propriétaire. Requis si visibility: internal, interdit si visibility: private, optionnel (attribution + contrôle de publication) si visibility: public.
tagsliste[chaîne]nullPour la recherche/catégorisation dans la liste des packages.
engineobjetnullOutil tiers encapsulé ({name, version}). Les packages purement Python le laissent à null.
resourcesobjetnullRessources par défaut héritées par chaque fonction — voir plus bas.

engine — distinguer le wrapper de l’outil encapsulé

Section intitulée « engine — distinguer le wrapper de l’outil encapsulé »

Quand un package encapsule un binaire tiers (code-aster, abaqus, openfoam…), deux numéros de version coexistent : la version du package MecaPy (package.version, p. ex. 0.4.1) et la version de l’outil tiers embarquée dans l’image (p. ex. code-aster 17.4.0). Les descriptions en texte libre perdent vite cette distinction. Déclarez l’engine explicitement pour que la plateforme l’affiche comme un badge distinct :

package:
name: code-aster
version: 0.4.1
engine:
name: code-aster
version: "17.4.0"
ChampTypeRequis si présentNotes
engine.namechaîneoui1–50 caractères. Identifiant de l’engine (code-aster, abaqus, openfoam, …).
engine.versionchaîneoui1–50 caractères. Chaîne de version libre (semver, année, SHA git — ce qu’utilise l’outil tiers).

Le bloc est tout-ou-rien — déclarez les deux champs ou omettez le bloc entier. Le frontend l’affiche comme un badge sur la page de détail du package, la page d’exécution (engine code-aster 17.4.0) et l’historique des jobs (traçabilité : on peut retrouver quel build du solveur a servi à une exécution donnée).

Le bloc est une union discriminée sur kind. Chaque kind a ses propres champs requis :

runtime:
kind: python # mode A — managé
version: "3.12" # requis, "3.10" | "3.11" | "3.12"
requirements: false # optionnel, installe le requirements.txt racine au build
runtime:
kind: dockerfile # mode B — build custom
dockerfile: Dockerfile # requis, chemin relatif à la racine du repo
context: . # optionnel, défaut "."
runtime:
kind: image # mode C — image pré-construite
image: ghcr.io/me/x:1.4 # requis, référence registry complète avec tag

Détails et compromis par mode : modes de runtime.

Un dict non vide. Chaque entrée déclare exactement un corps parmi quatre, selon le mode de runtime et le style d’écriture :

functions:
my_function:
# ── Mode A, code à la main ──
handler: pkg.module:function # `module:function`
# ── Mode A, corps déclaratif (l'un OU l'autre, voir section dédiée) ──
formula:
outputs: { area: "pi * radius ** 2" }
# table:
# columns: { d: [6, 8, 10], As: [20.1, 36.6, 58.0] }
# axes: { d: { kind: continuous, interpolation: true } }
# ── Modes B/C uniquement ──
entrypoint: ["python", "/run.py", "x"] # liste argv, non vide
# ── Commun ──
description: |
Description en texte libre.
inputs:
diameter: { type: Length, required: true, description: "..." }
load: { type: Force, symbol: "F_t", unit: "N" }
outputs:
stress: { type: Stress }
resources:
recommended: { cpu: 1, memory_mb: 512 }
timeout: 120
testcases: tests/static_support.json # chemin optionnel vers un JSON de cas de test
ChampTypeRequisNotes
handlerchaîneun corps requismodule:function. Fonctions de niveau module uniquement — voir handlers.
formulaobjetun corps requisCorps déclaratif : expressions mathématiques, handler généré par la plateforme. Voir ci-dessous.
tableobjetun corps requisCorps déclaratif : table de valeurs (lookup ± interpolation 1-D). Voir ci-dessous.
entrypointliste[chaîne]un corps requisModes B/C. Liste argv, non vide. Le conteneur exécute entrypoint[0] entrypoint[1] ....
descriptionchaînenonTexte libre.
inputsobjetnonPorts d’entrée typés. Voir ports d’E/S typés.
outputsobjetnonPorts de sortie typés.
resourcesobjetnonSurcharge par fonction de package.resources.
testcaseschaînenonChemin (relatif à la racine du repo) d’un fichier JSON de cas de test — exécuté après le build quand quality.validation_testcases.on_deployment vaut true.

Les quatre corps (handler, formula, table, entrypoint) sont mutuellement exclusifs : exactement un par fonction. Le recoupement avec runtime.kind se fait après le parsing — les corps handler, formula et table exigent le mode A (runtime Python), les modes B/C exigent entrypoint.

En mode A avec handler:, vous pouvez omettre entièrement inputs: / outputs: — MecaPy lit la signature du handler et les déduit des type hints. Vous ne les déclarez que pour surcharger ou documenter. Avec un corps déclaratif ou en modes B/C ils sont obligatoires : il n’y a pas de signature Python à lire.

Le manifeste est la source unique d’une fonction déclarative : la plateforme génère le handler Python (handler_generated.py, nom réservé — ne placez aucun fichier de ce nom dans le package) au moment du build, à partir du bloc seul. L’éditeur en ligne (mode Simple) édite ces blocs directement, et sait convertir une formule en fichier Python (« Convertir en code ») quand vous voulez reprendre la main.

functions:
preload_max:
description: Précharge maximale admissible.
inputs:
T_max: { type: Moment, symbol: "T_{max}", unit: "N*m" }
B: { type: Area, unit: "m^2" }
outputs:
F0_max: { type: Force, unit: "N" }
formula:
intermediates: # optionnel — évaluées dans l'ordre
k: "T_max * 1000"
outputs: # requis, non vide
F0_max: "k / B"

Contraintes validées à l’édition (erreur FORMULA_INVALID) et au build :

  • le nom de la fonction doit être un identifiant Python (lettres, chiffres, underscore — pas de tiret) : il devient def <nom>( dans le handler généré ;
  • chaque expression est du Python (mode expression) et ne référence que les entrées, les intermédiaires et l’allowlist mathématique (sin, cos, sqrt, exp, log, pi, e, abs, min, max, round, …) ;
  • les clés de formula.outputs et les ports outputs: doivent coïncider exactement (l’éditeur Simple maintient cet alignement automatiquement).

Un bloc table: déclare des colonnes de même longueur ; chaque entrée est un axe categorical (correspondance exacte, typé Literal[...]) ou continuous (avec interpolation: true pour l’interpolation linéaire 1-D) ; les colonnes restantes sont les sorties.

Le bloc resources déclare des ressources brutes — CPU, mémoire, timeout. Il ne mentionne aucun tier : un tier est une notion d’infrastructure (formes de VM, propres au fournisseur cloud), alors qu’un manifeste doit rester stable quand l’infra évolue. Les tiers sont calculés par la plateforme à partir de ces valeurs.

resources:
recommended: # requis — ressources visées par défaut
cpu: 2
memory_mb: 2048
minimal: # optionnel — plancher dur (CPU/RAM)
cpu: 1
memory_mb: 512
maximal: # optionnel — plafond dur (CPU/RAM)
cpu: 8
memory_mb: 16384
timeout: 300 # requis — secondes
ChampTypeRequisNotes
recommendedobjet {cpu, memory_mb}ouiRessources visées par défaut. Détermine le tier par défaut.
minimalobjet {cpu, memory_mb}nonPlancher dur de la fourchette d’override. Absent → pas de plancher.
maximalobjet {cpu, memory_mb}nonPlafond dur de la fourchette d’override. Absent → plafond plateforme.
timeoutentierouiSecondes. Axe indépendant du couple CPU/RAM.

cpu est un entier de cores, memory_mb un entier de Mio.

À l’exécution, la plateforme projette recommended sur son catalogue de tiers (le plus petit tier dont cpu et memory_mb couvrent le recommandé devient le tier par défaut). Les pages d’exécution et l’éditeur de workflow proposent un sélecteur de tier ; minimal / maximal, s’ils sont déclarés, en grisent les bornes hors fourchette.

Catalogue plateforme actuel (CPU / mémoire — le timeout n’est pas porté par le tier) :

TierCPUMémoire (Mio)
nano1512
micro11024
small22048
medium24096
large48192
xlarge416384
xlarge_2432768
xlarge_4865536
xlarge_816131072
xlarge_1632262144
xlarge_3264524288

Les tiers nanolarge s’exécutent sur le parc de workers courant. Les tiers xlargexlarge_32 sont des gabarits haute-mémoire ; un job qui en exige un ne s’exécute qu’une fois qu’un worker assez grand est disponible, sinon il est rejeté avec une erreur explicite (« tier exceeds worker capacity »).

Le plafond dur de la plateforme est xlarge_32 (64 CPU / 524288 Mio) ; le timeout est plafonné à 86400 s. Une valeur recommended / minimal / maximal ou un timeout au-delà déclenche une erreur de validation, de même que l’ordre minimal ≤ recommended ≤ maximal.

package.resources fixe le défaut. Si une fonction déclare son propre bloc resources:, celui-ci remplace entièrement le bloc package (pas de fusion sous-champ par sous-champ) — il doit donc être complet (recommended + timeout). L’absence des deux blocs retombe sur le défaut plateforme (recommended: {cpu: 2, memory_mb: 2048}, timeout: 300).

Un dict de forme libre. Son seul consommateur actuel est le contrôle par cas de test :

quality:
validation_testcases:
on_deployment: true # exécute les cas de test après le build
fail_on_error: false # gate la version sur échec (défaut false)

Quand on_deployment: true, après un build réussi le package passe à l’état validating : les cas de test du fichier JSON testcases: de chaque fonction sont exécutés, un job par cas. À la fin :

  • tous les cas passent → la version devient ready ;
  • un cas échoue avec fail_on_error: false (défaut) → la version devient quand même ready, les échecs étant consignés en avertissement ;
  • un cas échoue avec fail_on_error: true → la version passe à validation_failed et n’est pas servable.

Les résultats — réussite par fonction, cas en échec, lien vers le job de chaque cas — sont visibles sur la page de détail du package.

L’orchestrateur de validation est polling-based, pas push-based — il n’est pas réveillé par la fin du build. Conséquences à connaître :

  • Démarrage : un scheduler interne tique chaque minute et ramasse tous les packages en validating. Entre le moment où le build flippe le statut et le premier job pending visible dans l’historique, on observe donc jusqu’à ~60 s d’attente puis 1–4 s de préparation (download du zip source S3, extract, parse des fichiers testcases:).
  • Plafond agrégé par tick : un seul tick d’orchestration attend les jobs au maximum 15 min. Au-delà, les jobs encore pending sont comptés comme échoués (côté validation — ils peuvent finir côté worker, mais leur résultat n’est plus injecté dans la validation, qui transitionne immédiatement le package).
  • Plafond par cas : chaque job est en plus borné par le timeout de la fonction (par défaut 300 s, voir resources). Un cas qui excède ce timeout est tué par le worker et compte comme un échec.
  • Pas de boucle infinie : tant qu’un tick polle, les ticks suivants sont muselés par un verrou advisory Postgres et ne re-soumettent pas les cas. Le package quitte validating au plus tard 15 min après son ramassage initial.

Pratique : pour des cas de validation lents (FEM, mailleurs), pense à relever resources.timeout dans le manifeste et à découper le calcul en cas plus petits — l’orchestrateur agrège, mais reste limité à 15 min d’attente pour l’ensemble du package.

Le bloc est volontairement non typé au niveau du modèle pour que de nouvelles sous-clés puissent arriver sans changer le format du manifest. Tout ce qui est hors validation_testcases est aujourd’hui ignoré.

Le parseur s’exécute dans cet ordre — le connaître aide à lire les messages d’erreur :

  1. Chargement YAML — les erreurs de syntaxe viennent d’ici.
  2. Schéma — chaque champ est typé et contrôlé selon le modèle.
  3. Résolution du runtime discriminéruntime.kind choisit le sous-modèle qui parse le reste du bloc runtime.
  4. handler OU EXCLUSIF entrypoint — validation au niveau fonction.
  5. Recoupement runtime.kind ↔ champ par fonction — validation au niveau manifest.
  6. Plafonds de ressources — chaque bloc resources: doit respecter les plafonds plateforme (64 CPU / 524288 Mio / 86400 s) et l’ordre minimal ≤ recommended ≤ maximal.
  7. Résolution des E/S typées — chaque type: est cherché dans le catalogue de types ; un type inconnu échoue.
  8. Introspection de signature en mode A (au déploiement uniquement) — le handler est importé et sa signature lue pour les schemas déduits des type hints.

Les erreurs des étapes 1 à 7 sont renvoyées de façon synchrone en 4xx par la route de déploiement. Les erreurs de l’étape 8 retombent sur un schema permissif {"type": "object"} avec un avertissement journalisé côté serveur, plutôt que de faire échouer le déploiement.