diff --git a/README.md b/README.md index eb625580405d814381aaef21755f4aac6caebba9..8a26e674b5ded1ddbb2b98776d3ed110408cdc41 100644 --- a/README.md +++ b/README.md @@ -229,3 +229,19 @@ Pour s'aider, on peut afficher la valeur dans la console : ```js $: console.log(parameterSmicMensuel, smicValue) ``` + +### Générer les YAML de test OpenFisca à partir des cas-type LexImpact + +```bash +npx tsx src/scripts/generate_openfisca_tests_yaml.ts -y 2025 ../../openfisca-france/tests/leximpact/ +``` + +- Le paramètre `year` générera les variables d'output pour l'année `year` +- L'argument par défaut `outdir` est le chemin où l'on veut exporter les YAML générés. Ceux-ci ont vocation à être pushés dans openfisca_france/tests/leximpact + +Les YAML générés contiennent, pour chaque cas-type, toutes les variables d'entrées du cas-type dans le fichier `test_cases.json`. +Les valeurs sont mensualisées pour toutes les variables mensuelles. + +La section `output:` du YAML contient toutes les variables calculées, présentes à la fois dans les décompositions LexImpact **et** dans les variables du country-package Openfisca-France. + +Les valeurs des variables de type Enum, sont remplacées par leur index dans les valeurs possibles de l'Enum. diff --git a/src/scripts/generate_openfisca_tests_yaml.ts b/src/scripts/generate_openfisca_tests_yaml.ts index f64630a3bfbce8977743ede0c760d721bd4581fb..ee781d75d93ca3893bf7b4a6b8ee82ceef7e5a8a 100644 --- a/src/scripts/generate_openfisca_tests_yaml.ts +++ b/src/scripts/generate_openfisca_tests_yaml.ts @@ -10,8 +10,13 @@ import { variableSummaryByName, } from "$lib/variables" import { nonVirtualVariablesName } from "$lib/decompositions" -import type { EntityByKey } from "@openfisca/json-model" -import { entityByKey } from "$lib/entities" +import type { + EntityByKey, + Entity, + GroupEntity, + PersonEntity, +} from "@openfisca/json-model" +import { entityByKey, personEntityKey } from "$lib/entities" type JsonObject = { [key: string]: any } @@ -43,6 +48,36 @@ const optionsDefinitions = [ ] const options = commandLineArgs(optionsDefinitions) +async function fetchWithRetries( + url: string, + options: RequestInit, + maxRetries = 5, + delay = 10000, +) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const response = await fetch(url, options) + + if (response.ok) { + return response + } + + console.warn( + `Calling Openfisca API. Attempt ${attempt} : Failed with status ${response.status}`, + ) + } catch (error) { + console.error(`Error on attempt ${attempt} :`, error) + } + + if (attempt < maxRetries) { + console.log(`New attempt in ${delay / 1000}s...`) + await new Promise((res) => setTimeout(res, delay)) + } + } + + throw new Error(`Failed calling OpenFisca API after ${maxRetries} attempts`) +} + function removeSpacesFromKeys( obj: JsonObject, replacedKeys: { [key: string]: string }, @@ -180,12 +215,25 @@ function splitVariablesByCategory( for (const [variable, value] of Object.entries(entityData)) { if (outputVariablesList.includes(variable)) { - output[variable] = value + if ( + variableSummaryByName[variable].value_type === "Enum" && + variableSummaryByName[variable].possible_values !== undefined + ) { + // Replace Enum values by their index in possible_values + const newValue: Record<string, number> = {} + for (const [period, periodValue] of Object.entries(value)) { + newValue[period] = Object.keys( + variableSummaryByName[variable].possible_values, + ).indexOf(periodValue as string) + } + output[variable] = newValue + } else { + output[variable] = value + } } else { input[variable] = value } } - return { input, output } } @@ -223,11 +271,13 @@ function buildYamlOutput( const jsonForYamlOutput: { name: string period: string + max_spiral_loops: number input: JsonObject output: JsonObject } = { name: name, period: year, + max_spiral_loops: 4, input: {}, output: {}, } @@ -289,7 +339,43 @@ function getEntityKeyFromKeyPlural( return null } +function isGroupEntity(entity: Entity): entity is GroupEntity { + return (entity as GroupEntity).roles !== undefined +} + +function isKeyOrPlural(str: string): boolean { + const keys = Object.keys(entityByKey) + + for (const key of keys) { + const entity = entityByKey[key] + if (entity.key === str || entity.key_plural === str) { + return true + } + if (isGroupEntity(entity) && entity.roles) { + for (const role of entity.roles) { + if (role.key === str || role.key_plural === str) { + return true + } + if (role.subroles) { + for (const subrole of role.subroles) { + if (subrole.key === str || subrole.key_plural === str) { + return true + } + } + } + } + } + } + + return false +} + async function main() { + console.info("Initializing...") + const personsEntityKey = Object.entries(entityByKey) + .filter(([, entity]) => entity.is_person) + .map(([key]) => key)[0] + const year = options.year const outdir = options.outdir @@ -323,6 +409,8 @@ async function main() { const testCasesCore = testCasesCoreUnknown as unknown as Situation[] for (const testCase of testCasesCore) { + console.info("Processing ", testCase.id) + let testCaseContainsMissingVariable = false let jsonForApiCall: JsonObject = {} const outputVariablesList: Array<string> = [] @@ -332,6 +420,7 @@ async function main() { const sections = ["familles", "foyers_fiscaux", "menages", "individus"] + // Mensualise monthly input variables for (const section of sections) { if (testCase[section] !== undefined) { for (const entityKey in testCase[section]) { @@ -344,12 +433,27 @@ async function main() { testCase[section][entityKey][variable], variable, ) + } else if ( + !openFiscaVariablesList.hasOwnProperty(variable) && + !isKeyOrPlural(variable) + ) { + console.warn( + "variable", + variable, + "does not exists in openFisca-France.", + ) + testCaseContainsMissingVariable = true } } } } } + if (testCaseContainsMissingVariable) { + console.warn("Ignoring test case", testCase.id) + continue + } + for (const section of sections) { if (testCase[section] !== undefined) { jsonForApiCall.input[section] = removeSpacesFromKeys( @@ -397,205 +501,41 @@ async function main() { } } - // { - // // INDIVIDUS - // if (testCase.individus !== undefined) { - // jsonForApiCall.input.individus = removeSpacesFromKeys( - // testCase.individus, - // replacedKeys, - // ) - // for (const individuKey in jsonForApiCall.input.individus) { - // for (const variable of variablesToCalculate) { - // // console.log(variable) - // if (variableSummaryByName[variable] !== undefined) { - // if ( - // variableSummaryByName[variable].entity === "individu" && - // jsonForApiCall.input.individus[individuKey][variable] === - // undefined - // ) { - // outputVariablesList.push(variable) - // switch (variableSummaryByName[variable].definition_period) { - // case "year": { - // jsonForApiCall.input.individus[individuKey][variable] = { - // [year]: null, - // } - // break - // } - // case "month": { - // const monthVariableSection = {} - // for (let month = 1; month <= 12; month++) { - // const monthString = month < 10 ? "0" + month : month - // const monthPeriod = year + "-" + monthString - // Object.assign(monthVariableSection, { [monthPeriod]: null }) - // } - // jsonForApiCall.input.individus[individuKey][variable] = - // monthVariableSection - // } - // } - // } - // } else { - // console.log("cette variable n'existe pas dans summary : ", variable) - // } - // } - // } - // } else { - // console.error("No section 'individus' in file ", testCase) - // } - - // // MENAGES - // if (testCase.menages !== undefined) { - // jsonForApiCall.input.menages = removeSpacesFromKeys( - // testCase.menages, - // replacedKeys, - // ) - // for (const menageKey in jsonForApiCall.input.menages) { - // for (const variable of variablesToCalculate) { - // if (variableSummaryByName[variable] !== undefined) { - // if (variableSummaryByName[variable].entity === "menage") { - // outputVariablesList.push(variable) - // switch (variableSummaryByName[variable].definition_period) { - // case "year": { - // jsonForApiCall.input.menages[menageKey][variable] = { - // [year]: null, - // } - // break - // } - // case "month": { - // const monthVariableSection = {} - // for (let month = 1; month <= 12; month++) { - // const monthString = month < 10 ? "0" + month : month - // const monthPeriod = year + "-" + monthString - // Object.assign(monthVariableSection, { [monthPeriod]: null }) - // } - // jsonForApiCall.input.menages[menageKey][variable] = - // monthVariableSection - // } - // } - // } - // } else { - // // console.log("cette variable n'existe pas dans summary : ", variable) - // } - // } - // } - // } else { - // console.warn("No section 'menages' in file ", testCase) - // } - - // // FOYERS FISCAUX - // if (testCase.foyers_fiscaux !== undefined) { - // jsonForApiCall.input.foyers_fiscaux = removeSpacesFromKeys( - // testCase.foyers_fiscaux, - // replacedKeys, - // ) - // for (const foyerFiscalKey in jsonForApiCall.input.foyers_fiscaux) { - // for (const variable of variablesToCalculate) { - // if (variableSummaryByName[variable] !== undefined) { - // if (variableSummaryByName[variable].entity === "foyer_fiscal") { - // outputVariablesList.push(variable) - // switch (variableSummaryByName[variable].definition_period) { - // case "year": { - // jsonForApiCall.input.foyers_fiscaux[foyerFiscalKey][ - // variable - // ] = { - // [year]: null, - // } - // break - // } - // case "month": { - // const monthVariableSection = {} - // for (let month = 1; month <= 12; month++) { - // const monthString = month < 10 ? "0" + month : month - // const monthPeriod = year + "-" + monthString - // Object.assign(monthVariableSection, { [monthPeriod]: null }) - // } - // jsonForApiCall.input.foyers_fiscaux[foyerFiscalKey][ - // variable - // ] = monthVariableSection - // } - // } - // } - // } else { - // // console.log("cette variable n'existe pas dans summary : ", variable) - // } - // } - // } - // } else { - // console.warn("No section 'foyers_fiscaux' in file ", testCase) - // } - - // // FAMILLES - // if (testCase.familles !== undefined) { - // jsonForApiCall.input.familles = removeSpacesFromKeys( - // testCase.familles, - // replacedKeys, - // ) - // for (const familleKey in jsonForApiCall.input.familles) { - // for (const variable of variablesToCalculate) { - // if (variableSummaryByName[variable] !== undefined) { - // if (variableSummaryByName[variable].entity === "famille") { - // outputVariablesList.push(variable) - // switch (variableSummaryByName[variable].definition_period) { - // case "year": { - // jsonForApiCall.input.familles[familleKey][variable] = { - // [year]: null, - // } - // break - // } - // case "month": { - // const monthVariableSection = {} - // for (let month = 1; month <= 12; month++) { - // const monthString = month < 10 ? "0" + month : month - // const monthPeriod = year + "-" + monthString - // Object.assign(monthVariableSection, { [monthPeriod]: null }) - // } - // jsonForApiCall.input.familles[familleKey][variable] = - // monthVariableSection - // } - // } - // } - // } else { - // // console.log("cette variable n'existe pas dans summary : ", variable) - // } - // } - // } - // } else { - // console.warn("No section 'familles' in file ", testCase) - // } - // } jsonForApiCall = replaceValuesFromReplacedKeys(jsonForApiCall, replacedKeys) - - // fs.writeFile( - // path.join(".", testCase.id + ".json"), - // JSON.stringify(jsonForApiCall.input, null, 2), - // ) - - const simulatedTestCaseFromOpenFiscaApi = await fetch( - "https://api.fr.openfisca.org/latest/calculate", - // "http://localhost:5000/calculate", - { - body: JSON.stringify(jsonForApiCall.input), - headers: { - Accept: "application/json", - "Content-Type": "application/json; charset=utf-8", - }, - method: "POST", + console.info("Launching simulation on OpenFisca France API...") + const openFiscaApiUrl = "https://api.fr.openfisca.org/latest/calculate" + const openFiscaApiOptions: RequestInit = { + body: JSON.stringify(jsonForApiCall.input), + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", }, - ) - if (!simulatedTestCaseFromOpenFiscaApi.ok) { - console.error( - `Error ${ - simulatedTestCaseFromOpenFiscaApi.status - } while calling Openfisca France API`, - ) - return + method: "POST", } + let simulatedTestCaseFromOpenFiscaApi: Response | null = null + try { + simulatedTestCaseFromOpenFiscaApi = await fetchWithRetries( + openFiscaApiUrl, + openFiscaApiOptions, + ) + + if (!simulatedTestCaseFromOpenFiscaApi.ok) { + console.error( + `Giving up calling openFisca API because of status ${simulatedTestCaseFromOpenFiscaApi.status}`, + ) + return + } + } catch (error) { + console.error("Error : ", error) + } + console.info("Post-processing...") const simulatedTestCase = cleanSimulatedJson( - await simulatedTestCaseFromOpenFiscaApi.json(), + await simulatedTestCaseFromOpenFiscaApi!.json(), ) const jsonForYamlOutput = buildYamlOutput( - testCase.id, + testCase.id!, year, simulatedTestCase, outputVariablesList, @@ -609,6 +549,7 @@ async function main() { // Remove quotes around year keys const cleanedYaml = yamlOutput.replace(/'(\d{4})':/g, "$1:") await fs.writeFile(path.join(outdir, testCase.id + ".yml"), cleanedYaml) + console.info("YAML file written :", outdir, testCase.id + ".yml") } }