diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..d952357 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,25 @@ +Hello hello ! + +Je suis le fan numéro un d'OpenFisca, mais je viens de rencontrer un problème. + +### Qu'ai-je fait ? + + +### À quoi m'attendais-je ? + + +### Que s'est-il passé en réalité ? + + +### Voici des informations qui peuvent aider à reproduire le problème : + + +### Contexte + +Je m'identifie plus en tant que : + +- [ ] Contributeur·e : je contribue à OpenFisca Tunisia-Pension. +- [ ] Développeur·e : je crée des outils qui utilisent OpenFisca Tunisia-Pension. +- [ ] Économiste : je réalise des simulations avec des données. +- [ ] Mainteneur·e : j'intègre les contributions à OpenFisca Tunisia-Pension. +- [ ] Autre : _(ajoutez une description du contexte dans lequel vous utilisez OpenFisca)_. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..5128457 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +Merci de contribuer à OpenFisca ! Effacez cette ligne ainsi que, pour chaque ligne ci-dessous, les cas ne correspondant pas à votre contribution :) + +* Évolution du système socio-fiscal. | Amélioration technique. | Correction d'un crash. | Changement mineur. +* Périodes concernées : toutes. | jusqu'au JJ/MM/AAAA. | à partir du JJ/MM/AAAA. +* Zones impactées : `chemin/vers/le/fichier/contenant/les/variables/impactées`. +* Détails : + - Description de la fonctionnalité ajoutée ou du nouveau comportement adopté. + - Cas dans lesquels une erreur était constatée. + +- - - - + +Ces changements (effacez les lignes ne correspondant pas à votre cas) : + +- Modifient l'API publique d'OpenFisca Tunisia-Pension (par exemple renommage ou suppression de variables). +- Ajoutent une fonctionnalité (par exemple ajout d'une variable). +- Corrigent ou améliorent un calcul déjà existant. +- Modifient des éléments non fonctionnels de ce dépôt (par exemple modification du README). + +- - - - + +Quelques conseils à prendre en compte : + +- [ ] Jetez un coup d'œil au [guide de contribution](https://github.com/openfisca/openfisca-tunisia-pension-pension/blob/master/CONTRIBUTING.md). +- [ ] Regardez s'il n'y a pas une [proposition introduisant ces mêmes changements](https://github.com/openfisca/openfisca-tunisia-pension/pulls). +- [ ] Documentez votre contribution avec des références législatives. +- [ ] Mettez à jour ou ajoutez des tests correspondant à votre contribution. +- [ ] Augmentez le [numéro de version](https://speakerdeck.com/mattisg/git-session-2-strategies?slide=81) dans [`pyproject.toml`](https://github.com/openfisca/openfisca-tunisia-pension/blob/master/pyproject.toml). +- [ ] Mettez à jour le [`CHANGELOG.md`](https://github.com/openfisca/openfisca-tunisia-pension/blob/master/CHANGELOG.md). +- [ ] Assurez-vous de bien décrire votre contribution, comme indiqué ci-dessus + +Et surtout, n'hésitez pas à demander de l'aide ! :) diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5d9df5c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: monthly + labels: + - kind:dependencies + open-pull-requests-limit: 2 diff --git a/.github/get_minimal_version.py b/.github/get_minimal_version.py new file mode 100644 index 0000000..f4c431c --- /dev/null +++ b/.github/get_minimal_version.py @@ -0,0 +1,14 @@ +import re +import tomli +# This script prints the minimal version of Openfisca-Core to ensure their compatibility during CI testing +with open('./pyproject.toml', 'rb') as file: + config = tomli.load(file) + deps = config['project']['dependencies'] + for dep in deps: + version = re.search(r'openfisca-core\[([^\]]+)\]\s*>=\s*([\d\.]*)', dep) + if version: + try: + print(f'openfisca-core[{version[1]}]=={version[2]}') # noqa: T201 <- This is to avoid flake8 print detection. + except Exception as e: + print(f'Error processing "{dep}": {e}') # noqa: T201 <- This is to avoid flake8 print detection. + exit(1) diff --git a/.github/get_pypi_info.py b/.github/get_pypi_info.py new file mode 100755 index 0000000..87522f6 --- /dev/null +++ b/.github/get_pypi_info.py @@ -0,0 +1,51 @@ +import argparse +import requests +import logging + + +logging.basicConfig(level=logging.INFO) + + +def get_info(package_name: str = '') -> dict: + ''' + Get minimal informations needed by .conda/meta.yaml from PyPi JSON API. + ::package_name:: Name of package to get infos from. + ::return:: A dict with last_version, url and sha256 + ''' + if package_name == '': + raise ValueError('Package name not provided.') + resp = requests.get(f'https://pypi.org/pypi/{package_name}/json').json() + version = resp['info']['version'] + for v in resp['releases'][version]: + if v['packagetype'] == 'sdist': # for .tag.gz + return { + 'last_version': version, + 'url': v['url'], + 'sha256': v['digests']['sha256'] + } + + +def replace_in_file(filepath: str, info: dict): + ''' + ::filepath:: Path to meta.yaml, with filename + ::info:: Dict with information to populate + ''' + with open(filepath, 'rt') as fin: + meta = fin.read() + # Replace with info from PyPi + meta = meta.replace('PYPI_VERSION', info['last_version']) + meta = meta.replace('PYPI_URL', info['url']) + meta = meta.replace('PYPI_SHA256', info['sha256']) + with open(filepath, 'wt') as fout: + fout.write(meta) + logging.info(f'File {filepath} has been updated with informations from PyPi.') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-p', '--package', type=str, default='', required=True, help='The name of the package') + parser.add_argument('-f', '--filename', type=str, default='.conda/meta.yaml', help='Path to meta.yaml, with filename') + args = parser.parse_args() + info = get_info(args.package) + logging.info(f'Information of the last published PyPi package : {info}') + replace_in_file(args.filename, info) diff --git a/.github/has-functional-changes.sh b/.github/has-functional-changes.sh new file mode 100755 index 0000000..48f9780 --- /dev/null +++ b/.github/has-functional-changes.sh @@ -0,0 +1,12 @@ +#! /usr/bin/env bash + +IGNORE_DIFF_ON="README.md CONTRIBUTING.md Makefile .gitignore .github/*" + +last_tagged_commit=`git describe --tags --abbrev=0 --first-parent` # --first-parent ensures we don't follow tags not published in master through an unlikely intermediary merge commit + +if git diff-index --name-only --exit-code $last_tagged_commit -- . `echo " $IGNORE_DIFF_ON" | sed 's/ / :(exclude)/g'` # Check if any file that has not be listed in IGNORE_DIFF_ON has changed since the last tag was published. +then + echo "No functional changes detected." + exit 1 +else echo "The functional files above were changed." +fi diff --git a/.github/is-version-number-acceptable.sh b/.github/is-version-number-acceptable.sh new file mode 100755 index 0000000..aff071c --- /dev/null +++ b/.github/is-version-number-acceptable.sh @@ -0,0 +1,45 @@ +#! /usr/bin/env bash + +if [[ ${GITHUB_REF#refs/heads/} == master ]] +then + echo "No need for a version check on master." + exit 0 +fi + +if ! $(dirname "$BASH_SOURCE")/has-functional-changes.sh +then + echo "No need for a version update." + exit 0 +fi + +current_version=$(python `dirname "$BASH_SOURCE"`/pyproject_version.py --only_package_version True) # parsing with tomllib is complicated, see https://github.com/python-poetry/poetry/issues/273 +if [ $? -eq 0 ]; then + echo "Package version in pyproject.toml : $current_version" +else + echo "ERROR getting current version: $current_version" + exit 3 +fi + +if [[ -z $current_version ]] +then + echo "Error getting current version" + exit 1 +fi + +if git rev-parse --verify --quiet $current_version +then + echo "Version $current_version already exists in commit:" + git --no-pager log -1 $current_version + echo + echo "Update the version number in pyproject.toml before merging this branch into master." + echo "Look at the CONTRIBUTING.md file to learn how the version number should be updated." + exit 2 +fi + +if ! $(dirname "$BASH_SOURCE")/has-functional-changes.sh | grep --quiet CHANGELOG.md +then + echo "CHANGELOG.md has not been modified, while functional changes were made." + echo "Explain what you changed before merging this branch into master." + echo "Look at the CONTRIBUTING.md file to learn how to write the changelog." + exit 2 +fi diff --git a/.github/lint-changed-python-files.sh b/.github/lint-changed-python-files.sh new file mode 100755 index 0000000..72d8aad --- /dev/null +++ b/.github/lint-changed-python-files.sh @@ -0,0 +1,11 @@ +#! /usr/bin/env bash + +last_tagged_commit=`git describe --tags --abbrev=0 --first-parent` # --first-parent ensures we don't follow tags not published in master through an unlikely intermediary merge commit + +if ! changes=$(git diff-index --name-only --diff-filter=ACMR --exit-code $last_tagged_commit -- "*.py") +then + echo "Linting the following Python files:" + echo $changes + flake8 $changes +else echo "No changed Python files to lint" +fi diff --git a/.github/lint-changed-yaml-tests.sh b/.github/lint-changed-yaml-tests.sh new file mode 100755 index 0000000..16e9943 --- /dev/null +++ b/.github/lint-changed-yaml-tests.sh @@ -0,0 +1,11 @@ +#! /usr/bin/env bash + +last_tagged_commit=`git describe --tags --abbrev=0 --first-parent` # --first-parent ensures we don't follow tags not published in master through an unlikely intermediary merge commit + +if ! changes=$(git diff-index --name-only --diff-filter=ACMR --exit-code $last_tagged_commit -- "tests/*.yaml") +then + echo "Linting the following changed YAML tests:" + echo $changes + yamllint $changes +else echo "No changed YAML tests to lint" +fi diff --git a/.github/publish-git-tag.sh b/.github/publish-git-tag.sh new file mode 100755 index 0000000..f60b980 --- /dev/null +++ b/.github/publish-git-tag.sh @@ -0,0 +1,5 @@ +#! /usr/bin/env bash + +current_version=$(grep '^version =' pyproject.toml | cut -d '"' -f 2) # parsing with tomllib is complicated, see https://github.com/python-poetry/poetry/issues/273 +git tag $current_version +git push --tags # update the repository version diff --git a/.github/pyproject_version.py b/.github/pyproject_version.py new file mode 100644 index 0000000..5da7842 --- /dev/null +++ b/.github/pyproject_version.py @@ -0,0 +1,82 @@ +# Read package version in pyproject.toml and replace it in .conda/recipe.yaml + +import argparse +import logging +import re + +logging.basicConfig(level=logging.INFO, format='%(message)s') +PACKAGE_VERSION = 'X.X.X' +CORE_VERSION = '>=43,<44' +NUMPY_VERSION = '>=1.24.3,<2' + + +def get_versions(): + ''' + Read package version and deps in pyproject.toml + ''' + openfisca_core_api = None + openfisca_tunisia_pension = None + with open('./pyproject.toml', 'r') as file: + content = file.read() + # Extract the version of openfisca_tunisia-pension + version_match = re.search(r'^version\s*=\s*"([\d.]*)"', content, re.MULTILINE) + if version_match: + openfisca_tunisia_pension = version_match.group(1) + else: + raise Exception('Package version not found in pyproject.toml') + # Extract dependencies + version = re.search(r'openfisca-core\[web-api\]\s*(>=\s*[\d\.]*,\s*<\d*)"', content, re.MULTILINE) + if version: + openfisca_core_api = version.group(1) + version = re.search(r'numpy\s*(>=\s*[\d\.]*,\s*<\d*)"', content, re.MULTILINE) + if version: + numpy = version.group(1) + if not openfisca_core_api or not numpy: + raise Exception('Dependencies not found in pyproject.toml') + return { + 'openfisca_tunisia_pension': openfisca_tunisia_pension, + 'openfisca_core_api': openfisca_core_api.replace(' ', ''), + 'numpy': numpy.replace(' ', ''), + } + + +def replace_in_file(filepath: str, info: dict): + ''' + ::filepath:: Path to meta.yaml, with filename + ::info:: Dict with information to populate + ''' + with open(filepath, 'rt') as fin: + meta = fin.read() + # Replace with info from pyproject.toml + if PACKAGE_VERSION not in meta: + raise Exception(f'{PACKAGE_VERSION=} not found in {filepath}') + meta = meta.replace(PACKAGE_VERSION, info['openfisca_tunisia-pension']) + if CORE_VERSION not in meta: + raise Exception(f'{CORE_VERSION=} not found in {filepath}') + meta = meta.replace(CORE_VERSION, info['openfisca_core_api']) + if NUMPY_VERSION not in meta: + raise Exception(f'{NUMPY_VERSION=} not found in {filepath}') + meta = meta.replace(NUMPY_VERSION, info['numpy']) + with open(filepath, 'wt') as fout: + fout.write(meta) + logging.info(f'File {filepath} has been updated with informations from pyproject.toml.') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-r', '--replace', type=bool, default=False, required=False, help='replace in file') + parser.add_argument('-f', '--filename', type=str, default='.conda/recipe.yaml', help='Path to recipe.yaml, with filename') + parser.add_argument('-o', '--only_package_version', type=bool, default=False, help='Only display current package version') + args = parser.parse_args() + info = get_versions() + file = args.filename + if args.only_package_version: + print(f'{info["openfisca_tunisia-pension"]}') # noqa: T201 + exit() + logging.info('Versions :') + print(info) # noqa: T201 + if args.replace: + logging.info(f'Replace in {file}') + replace_in_file(file, info) + else: + logging.info('Dry mode, no replace made') diff --git a/.github/split_tests.py b/.github/split_tests.py new file mode 100644 index 0000000..11e2b3f --- /dev/null +++ b/.github/split_tests.py @@ -0,0 +1,22 @@ +import sys +from glob import glob + + +def split_tests(number_of_files, CI_NODE_TOTAL, CI_NODE_INDEX, test_files_list): + test_files_sublist = [] + + for file_index in range(number_of_files): + file_number = file_index % CI_NODE_TOTAL + if file_number == CI_NODE_INDEX: + test_files_sublist.append(test_files_list[file_index]) + + tests_to_run_string = ' '.join(test_files_sublist) + + return tests_to_run_string + + +if __name__ == '__main__': + CI_NODE_TOTAL, CI_NODE_INDEX = int(sys.argv[1]), int(sys.argv[2]) + test_files_list = glob('tests/**/*.yaml', recursive=True) + glob('tests/**/*.yml', recursive=True) + number_of_files = len(test_files_list) + sys.stdout.write(split_tests(number_of_files, CI_NODE_TOTAL, CI_NODE_INDEX, test_files_list)) diff --git a/.github/test-api.sh b/.github/test-api.sh new file mode 100755 index 0000000..e7101b2 --- /dev/null +++ b/.github/test-api.sh @@ -0,0 +1,14 @@ +#! /usr/bin/env bash + +PORT=5000 +ENDPOINT=spec + +openfisca serve --country-package openfisca_tunisia-pension --port $PORT --workers 1 & +server_pid=$! + +curl --retry-connrefused --retry 10 --retry-delay 5 --fail http://127.0.0.1:$PORT/$ENDPOINT | python -m json.tool > /dev/null +result=$? + +kill $server_pid + +exit $? diff --git a/.github/workflows/tax-benefit.yml b/.github/workflows/tax-benefit.yml new file mode 100644 index 0000000..6bf03cf --- /dev/null +++ b/.github/workflows/tax-benefit.yml @@ -0,0 +1,93 @@ +name: Validate, integrate & deploy to tax-benefit.org + +on: + - push + - workflow_dispatch + +jobs: + validate_yaml: + uses: tax-benefit/actions/.github/workflows/validate_yaml.yml@v2.1.0 + with: + parameters_path: "openfisca_tunisia/parameters" + secrets: + token: ${{ secrets.CONTROL_CENTER_TOKEN }} + + deploy_parameters: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Clone Legislation Parameters Explorer + run: git clone https://git.leximpact.dev/leximpact/legislation-parameters-explorer.git + - name: Install Node.js version LTS + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - name: Install viewer dependencies + run: npm install + working-directory: legislation-parameters-explorer/packages/viewer + - name: Configure viewer + run: | + rm -f .env + cat > .env << EOF + # Customizations to apply to the site (theme, URLs…) + CUSTOMIZATION="openfisca" + + DBNOMICS_DATASET_CODE="openfisca_tunisia" + DBNOMICS_PROVIDER_CODE="OpenFisca" + DBNOMICS_URL="https://db.nomics.world/" + + EDITOR_URL="https://editor.parameters.tn.tax-benefit.org/" + + EXPORT_CSV=true + EXPORT_JSON=false + EXPORT_XLSX=true + + # Path of directory containing legislation parameters of country + PARAMETERS_DIR="../../../openfisca_tunisia/parameters/" + + # Description of parameters remote repository + PARAMETERS_AUTHOR_EMAIL="editor.parameters.tn@tax-benefit.org" + PARAMETERS_AUTHOR_NAME="Éditeur des paramètres d'OpenFisca-Tunisia" + PARAMETERS_BRANCH="main" + PARAMETERS_FORGE_DOMAIN_NAME="github.com" + PARAMETERS_FORGE_TYPE="GitHub" + PARAMETERS_GROUP="openfisca" + PARAMETERS_PROJECT="openfisca-tunisia-pension" + PARAMETERS_PROJECT_DIR="openfisca_tunisia/parameters" + + SHOW_LAST_BREADCRUMB_ITEM = false + + TABLE_OF_CONTENTS_DIR="../../../openfisca_tunisia/tables/" + + TITLE="OpenFisca-Tunisia - الجباية المفتوحة تونس" + + # Path of file containing units used by French legislation parameters + UNITS_FILE_PATH="../../../openfisca_tunisia/units.yaml" + EOF + working-directory: legislation-parameters-explorer/packages/viewer + - name: Initialize .svelte-kit directory of viewer + run: npx svelte-kit sync + working-directory: legislation-parameters-explorer/packages/viewer + - name: Generate data of viewer + run: npx tsx src/scripts/generate_data.ts + working-directory: legislation-parameters-explorer/packages/viewer + - name: Build viewer + run: npm run build + working-directory: legislation-parameters-explorer/packages/viewer + - name: Configure ssh for deployment to server + uses: tanmancan/action-setup-ssh-agent-key@1.0.0 + with: + ssh-auth-sock: /tmp/my_auth.sock + ssh-private-key: ${{ secrets.PARAMETERS_EXPLORER_SSH_PRIVATE_KEY }} + ssh-public-key: ${{ secrets.PARAMETERS_EXPLORER_SSH_KNOWN_HOSTS }} + - name: Deploy to Server using rsync + run: rsync -az --delete -e "ssh -J ssh-proxy@parameters.tn.tax-benefit.org:2222" build/ parameters.tn.tax-benefit.org@10.131.0.2:public_html/ + working-directory: legislation-parameters-explorer/packages/viewer + + deploy_simulator: + uses: tax-benefit/actions/.github/workflows/deploy.yml@v2.1.0 + with: + python_package: "openfisca_tunisia" + secrets: + token: ${{ secrets.CONTROL_CENTER_TOKEN }} diff --git a/.github/workflows/validate_yaml.yml b/.github/workflows/validate_yaml.yml new file mode 100644 index 0000000..ac97c47 --- /dev/null +++ b/.github/workflows/validate_yaml.yml @@ -0,0 +1,15 @@ +name: Validate YAML + +on: + push: + workflow_dispatch: + pull_request: + types: [opened, reopened] + +jobs: + validate_yaml: + uses: tax-benefit/actions/.github/workflows/validate_yaml.yml@v1 + with: + parameters_path: "openfisca_tunisia/parameters" + secrets: + token: ${{ secrets.CONTROL_CENTER_TOKEN }} diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..bc7f483 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,236 @@ +name: OpenFisca Tunisia + +on: + push: + pull_request: + types: [opened, reopened] + +env: + DEFAULT_PYTHON_VERSION: '3.10.6' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + os: ["ubuntu-20.04"] # On peut ajouter "macos-latest" si besoin + python-version: ["3.9.9", "3.10.6"] + openfisca-dependencies: [minimal, maximal] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Cache build + id: restore-build + uses: actions/cache@v4 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ github.sha }}-${{ matrix.os }}-${{ matrix.openfisca-dependencies }} + restore-keys: | # in case of a cache miss (systematically unless the same commit is built repeatedly), the keys below will be used to restore dependencies from previous builds, and the cache will be stored at the end of the job, making up-to-date dependencies available for all jobs of the workflow; see more at https://docs.github.com/en/actions/advanced-guides/caching-dependencies-to-speed-up-workflows#example-using-the-cache-action + build-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ matrix.os }} + build-${{ env.pythonLocation }}-${{ matrix.os }} + - name: Build package + run: make build + - name: Minimal version + if: matrix.openfisca-dependencies == 'minimal' + run: | # Installs the OpenFisca dependencies minimal version from pyproject.toml + pip install $(python ${GITHUB_WORKSPACE}/.github/get_minimal_version.py) + - name: Cache release + id: restore-release + uses: actions/cache@v4 + with: + path: dist + key: release-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ github.sha }}-${{ matrix.os }}-${{ matrix.openfisca-dependencies }} + + lint-files: + runs-on: ubuntu-20.04 + strategy: + matrix: + dependencies-version: [maximal] + needs: [ build ] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all the tags + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + - name: Cache build + id: restore-build + uses: actions/cache@v4 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ github.sha }}-ubuntu-20.04-${{ matrix.dependencies-version }} + - run: make check-syntax-errors + - run: make check-style + - name: Lint Python files + run: "${GITHUB_WORKSPACE}/.github/lint-changed-python-files.sh" + - name: Lint YAML tests + run: "${GITHUB_WORKSPACE}/.github/lint-changed-yaml-tests.sh" + + test-python: + runs-on: ${{ matrix.os }} + needs: [ build ] + strategy: + fail-fast: true + matrix: + os: [ "ubuntu-20.04" ] # On peut ajouter "macos-latest" si besoin + python-version: ["3.9.9", "3.10.6"] + openfisca-dependencies: [minimal, maximal] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Cache build + id: restore-build + uses: actions/cache@v4 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ github.sha }}-${{ matrix.os }}-${{ matrix.openfisca-dependencies }} + - run: | + shopt -s globstar + openfisca test --country-package openfisca_tunisia_pension tests/**/*.py + if: matrix.openfisca-dependencies != 'minimal' || matrix.python-version != '3.9.9' + + test-path-length: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + - name: Test max path length + run: make check-path-length + + test-yaml: + runs-on: ubuntu-20.04 + needs: [ build ] + strategy: + fail-fast: false + matrix: + # Set N number of parallel jobs to run tests on. Here we use 10 jobs + # Remember to update ci_node_index below to 0..N-1 + ci_node_total: [ 10 ] + ci_node_index: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] + openfisca-dependencies: [minimal, maximal] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + - name: Cache build + id: restore-build + uses: actions/cache@v4 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ github.sha }}-ubuntu-20.04-${{ matrix.openfisca-dependencies }} + - name: Split YAML tests + id: yaml-test + env: + CI_NODE_TOTAL: ${{ matrix.ci_node_total }} + CI_NODE_INDEX: ${{ matrix.ci_node_index }} + run: | + echo "TEST_FILES_SUBLIST=$(python "${GITHUB_WORKSPACE}/.github/split_tests.py" ${CI_NODE_TOTAL} ${CI_NODE_INDEX})" >> $GITHUB_ENV + - name: Run YAML test + run: | + openfisca test --country-package openfisca_tunisia_pension ${TEST_FILES_SUBLIST} + + test-api: + runs-on: ubuntu-20.04 + strategy: + matrix: + dependencies-version: [maximal] + needs: [ build ] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + - name: Cache build + id: restore-build + uses: actions/cache@v4 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ github.sha }}-ubuntu-20.04-${{ matrix.dependencies-version }} + - name: Test the Web API + run: "${GITHUB_WORKSPACE}/.github/test-api.sh" + + check-version-and-changelog: + runs-on: ubuntu-20.04 + needs: [ lint-files, test-python, test-yaml, test-api ] # Last job to run + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all the tags + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + - name: Check version number has been properly updated + run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" + + # GitHub Actions does not have a halt job option, to stop from deploying if no functional changes were found. + # We build a separate job to substitute the halt option. + # The `deploy` job is dependent on the output of the `check-for-functional-changes` job. + check-for-functional-changes: + runs-on: ubuntu-20.04 + if: github.ref == 'refs/heads/master' # Only triggered for the `master` or `main` branch + needs: [ check-version-and-changelog ] + outputs: + status: ${{ steps.stop-early.outputs.status }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all the tags + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + - id: stop-early + run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi + + deploy: + runs-on: ubuntu-20.04 + strategy: + matrix: + dependencies-version: [maximal] + needs: [ check-for-functional-changes ] + if: needs.check-for-functional-changes.outputs.status == 'success' + env: + PYPI_USERNAME: __token__ + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all the tags + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + - name: Cache build + id: restore-build + uses: actions/cache@v4 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ github.sha }}-ubuntu-20.04-${{ matrix.dependencies-version }} + - name: Cache release + id: restore-release + uses: actions/cache@v4 + with: + path: dist + key: release-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ github.sha }}-ubuntu-20.04-${{ matrix.dependencies-version }} + - name: Upload a Python package to PyPi + run: twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN + - name: Publish a git tag + run: "${GITHUB_WORKSPACE}/.github/publish-git-tag.sh" diff --git a/Makefile b/Makefile index 5049e48..333cd28 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,53 @@ all: test -check-no-prints: - @test -z "`git grep -w print openfisca_tunisia_pension/model`" - -check-syntax-errors: - python -m compileall -q . +uninstall: + pip freeze | grep -v "^-e" | xargs pip uninstall -y clean: rm -rf build dist - find . -name '*.mo' -exec rm \{\} \; find . -name '*.pyc' -exec rm \{\} \; -flake8: +deps: + pip install --upgrade pip build twine + +install: deps + @# Install OpenFisca-Tunisia-Pension for development. + @# `make install` installs the editable version of OpenFisca-Tunisia. + @# This allows contributors to test as they code. + pip install --editable .[dev] --upgrade + pip install openfisca-core[web-api] + +build: clean deps + @# Install OpenFisca-Tunisia-Pension for deployment and publishing. + @# `make build` allows us to be be sure tests are run against the packaged version + @# of OpenFisca-Tunisia-Pension, the same we put in the hands of users and reusers. + python -m build + pip uninstall --yes openfisca-tunisia-pension + find dist -name "*.whl" -exec pip install {}[dev] \; + pip install openfisca-core[web-api] + +check-syntax-errors: + python -m compileall -q . + +format-style: + @# Do not analyse .gitignored files. + @# `make` needs `$$` to output `$`. Ref: http://stackoverflow.com/questions/2382764. + autopep8 `git ls-files | grep "\.py$$"` + +check-style: @# Do not analyse .gitignored files. @# `make` needs `$$` to output `$`. Ref: http://stackoverflow.com/questions/2382764. flake8 `git ls-files | grep "\.py$$"` -test: check-syntax-errors check-no-prints +check-yaml: + @# check yaml style + .github/lint-changed-yaml-tests.sh + +check-all-yaml: + @# check yaml style + yamllint . + +test: clean check-syntax-errors check-style @# Launch tests from openfisca_tunisia_pension/tests directory (and not .) because TaxBenefitSystem must be initialized @# before parsing source files containing formulas. - nosetests openfisca_tunisia_pension/tests --exe --with-doctest + openfisca test --country-package openfisca_tunisia_pension openfisca_tunisia_pension/tests diff --git a/README.md b/README.md index 86f06d1..0e7aeff 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ OpenFisca est un projet de logiciel libre. Son code source est distribué sous la licence [GNU Affero General Public Licence](http://www.gnu.org/licenses/agpl.html) -version 3 ou ultérieure (cf. [LICENSE](https://github.com/openfisca/openfisca-tunisia/blob/master/LICENSE)). +version 3 ou ultérieure (cf. [LICENSE](https://github.com/openfisca/openfisca-tunisia-pension/blob/master/LICENSE)). N'hésitez pas à rejoindre l'équipe de développement OpenFisca ! Pour en savoir plus, une [documentation](https://doc.openfisca.fr/contribute/index.html) est à votre disposition. @@ -24,7 +24,7 @@ N'hésitez pas à rejoindre l'équipe de développement OpenFisca ! Pour en savo
تم توزيع مصدر هذا البرنامج تحت رخصة أفيرو العامة الثالثة أو ما أعلى
-تعالو انضمو إلى فريق الجباية المفتوحة و ساهمو في تطوير البرنامج! +
تعالو انضمو إلى فريق الجباية المفتوحة و ساهمو في تطوير البرنامج! انظرو للموقع الرسمي للمزيد من المعلومات
@@ -32,7 +32,7 @@ N'hésitez pas à rejoindre l'équipe de développement OpenFisca ! Pour en savo OpenFisca is a free software project. Its source code is distributed under the [GNU Affero General Public Licence](http://www.gnu.org/licenses/agpl.html) -version 3 or later (see [LICENSE](https://github.com/openfisca/openfisca-tunisia/blob/master/LICENSE) file). +version 3 or later (see [LICENSE](https://github.com/openfisca/openfisca-tunisia-pension/blob/master/LICENSE) file). Feel free to join the OpenFisca development team! See the [documentation](https://doc.openfisca.fr/contribute/index.html) for more information. @@ -76,7 +76,7 @@ Ensuite, afin de créer un environnement de travail propre et pour vous permettr sudo pip install pew ``` -Il vous est désormais possible de créer votre premier environnement dédié à OpenFisca-Tunisia Pension. +Il vous est désormais possible de créer votre premier environnement dédié à OpenFisca-Tunisia Pension. ### Création d'environnement virtuel @@ -108,7 +108,7 @@ cd openfisca-tunisia-pension pip install -e . ``` -:tada: Félicitations, vous avez désormais terminé l'installation d'OpenFisca Tunisia Pension ! +:tada: Félicitations, vous avez désormais terminé l'installation d'OpenFisca Tunisia Pension ! Vous pouvez vérifier que votre environnement fonctionne bien en démarrant les tests tel que décrit dans le paragraphe suivant. @@ -144,7 +144,7 @@ Le format d'un test yaml est décrit dans la [documentation officielle](https:// Ainsi, si vous souhaitez exécuter le test yaml `openfisca_tunisia_pension/tests/formulas/pension_rsna.yaml`, utilisez la commande : ``` -openfisca-run-test -c openfisca_tunisia_pension openfisca_tunisia_pension/tests/formulas/pension_rsna.yaml +openfisca-run-test -c openfisca_tunisia_pension openfisca_tunisia_pension/tests/formulas/pension_rsna.yaml ``` ### Tout tester diff --git a/openfisca_tunisia_pension/__init__.py b/openfisca_tunisia_pension/__init__.py index b5673c4..6b04e0a 100644 --- a/openfisca_tunisia_pension/__init__.py +++ b/openfisca_tunisia_pension/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - -from tunisia_pension_taxbenefitsystem import TunisiaPensionTaxBenefitSystem +from openfisca_tunisia_pension.tunisia_pension_taxbenefitsystem import TunisiaPensionTaxBenefitSystem CountryTaxBenefitSystem = TunisiaPensionTaxBenefitSystem diff --git a/openfisca_tunisia_pension/entities.py b/openfisca_tunisia_pension/entities.py index 7bac2fc..aceb7a2 100644 --- a/openfisca_tunisia_pension/entities.py +++ b/openfisca_tunisia_pension/entities.py @@ -1,60 +1,57 @@ -# -*- coding: utf-8 -*- - - from openfisca_core.entities import build_entity Individu = build_entity( - key = "individu", - plural = "individus", - label = u'Individu', + key = 'individu', + plural = 'individus', + label = 'Individ', is_person = True ) FoyerFiscal = build_entity( - key = "foyer_fiscal", - plural = "foyers_fiscaux", - label = u'Déclaration d’impôts', + key = 'foyer_fiscal', + plural = 'foyers_fiscaux', + label = 'Déclaration d’impôts', roles = [ { 'key': 'declarant', 'plural': 'declarants', - 'label': u'Déclarants', + 'label': 'Déclarants', 'subroles': ['declarant_principal', 'conjoint'] }, { 'key': 'personne_a_charge', 'plural': 'personnes_a_charge', - 'label': u'Personnes à charge' + 'label': 'Personnes à charge' }, ] ) Menage = build_entity( - key = "menage", - plural = "menages", - label = u'Logement principal', + key = 'menage', + plural = 'menages', + label = 'Logement principal', roles = [ { 'key': 'personne_de_reference', - 'label': u'Personne de référence', + 'label': 'Personne de référence', 'max': 1 }, { 'key': 'conjoint', - 'label': u'Conjoint', + 'label': 'Conjoint', 'max': 1 }, { 'key': 'enfant', 'plural': 'enfants', - 'label': u'Enfants', + 'label': 'Enfants', 'max': 2 }, { 'key': 'autre', 'plural': 'autres', - 'label': u'Autres' + 'label': 'Autres' } ] ) diff --git a/openfisca_tunisia_pension/model/base.py b/openfisca_tunisia_pension/model/base.py deleted file mode 100644 index 0f51f31..0000000 --- a/openfisca_tunisia_pension/model/base.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- - - -from datetime import date -from openfisca_core.model_api import * -from openfisca_tunisia_pension.entities import FoyerFiscal, Individu, Menage diff --git a/openfisca_tunisia_pension/parameters/param_gen/index.yaml b/openfisca_tunisia_pension/parameters/marche_travail/index.yaml similarity index 100% rename from openfisca_tunisia_pension/parameters/param_gen/index.yaml rename to openfisca_tunisia_pension/parameters/marche_travail/index.yaml diff --git a/openfisca_tunisia_pension/parameters/param_gen/smag.yaml b/openfisca_tunisia_pension/parameters/marche_travail/smag.yaml similarity index 100% rename from openfisca_tunisia_pension/parameters/param_gen/smag.yaml rename to openfisca_tunisia_pension/parameters/marche_travail/smag.yaml diff --git a/openfisca_tunisia_pension/parameters/param_gen/smig_40h.yaml b/openfisca_tunisia_pension/parameters/marche_travail/smig_40h.yaml similarity index 100% rename from openfisca_tunisia_pension/parameters/param_gen/smig_40h.yaml rename to openfisca_tunisia_pension/parameters/marche_travail/smig_40h.yaml diff --git a/openfisca_tunisia_pension/parameters/param_gen/smig_48h.yaml b/openfisca_tunisia_pension/parameters/marche_travail/smig_48h.yaml similarity index 100% rename from openfisca_tunisia_pension/parameters/param_gen/smig_48h.yaml rename to openfisca_tunisia_pension/parameters/marche_travail/smig_48h.yaml diff --git a/openfisca_tunisia_pension/scenarios.py b/openfisca_tunisia_pension/scenarios.py deleted file mode 100644 index 917df7c..0000000 --- a/openfisca_tunisia_pension/scenarios.py +++ /dev/null @@ -1,641 +0,0 @@ -# -*- coding: utf-8 -*- - - -import collections -import datetime -import itertools -import logging -import re -import uuid - -from openfisca_core import conv, scenarios -from entities import Individu, FoyerFiscal, Menage - - -def N_(message): - return message - - -log = logging.getLogger(__name__) -year_or_month_or_day_re = re.compile(ur'(18|19|20)\d{2}(-(0[1-9]|1[0-2])(-([0-2]\d|3[0-1]))?)?$') - - -class Scenario(scenarios.AbstractScenario): - def init_single_entity(self, axes = None, enfants = None, foyer_fiscal = None, menage = None, - parent1 = None, parent2 = None, period = None): - if enfants is None: - enfants = [] - assert parent1 is not None - foyer_fiscal = foyer_fiscal.copy() if foyer_fiscal is not None else {} - individus = [] - menage = menage.copy() if menage is not None else {} - for index, individu in enumerate([parent1, parent2] + (enfants or [])): - if individu is None: - continue - id = individu.get('id') - if id is None: - individu = individu.copy() - individu['id'] = id = 'ind{}'.format(index) - individus.append(individu) - if index <= 1: - foyer_fiscal.setdefault('declarants', []).append(id) - if index == 0: - menage['personne_de_reference'] = id - else: - menage['conjoint'] = id - else: - foyer_fiscal.setdefault('personnes_a_charge', []).append(id) - menage.setdefault('enfants', []).append(id) - conv.check(self.make_json_or_python_to_attributes())(dict( - axes = axes, - period = period, - test_case = dict( - foyers_fiscaux = [foyer_fiscal], - individus = individus, - menages = [menage], - ), - )) - return self - - def post_process_test_case(self, test_case, period, state): - - individu_by_id = { - individu['id']: individu - for individu in test_case['individus'] - } - - parents_id = set( - parent_id - for foyer_fiscal in test_case['foyers_fiscaux'] - for parent_id in foyer_fiscal['declarants'] - ) - test_case, error = conv.struct( - dict( - # foyers_fiscaux = conv.pipe( - # conv.uniform_sequence( - # conv.struct( - # dict( - # enfants = conv.uniform_sequence( - # conv.test( - # lambda individu_id: - # individu_by_id[individu_id].get('invalide', False) - # or find_age(individu_by_id[individu_id], period.start.date, - # default = 0) <= 25, - # error = u"Une personne à charge d'un foyer fiscal doit avoir moins de" - # u" 25 ans ou être handicapée", - # ), - # ), - # parents = conv.pipe( - # conv.empty_to_none, - # conv.not_none, - # conv.test(lambda parents: len(parents) <= 2, - # error = N_(u'A "famille" must have at most 2 "parents"')) - # ), - # ), - # default = conv.noop, - # ), - # ), - # conv.empty_to_none, - # conv.not_none, - # ), - foyers_fiscaux = conv.pipe( - conv.uniform_sequence( - conv.struct( - dict( - declarants = conv.pipe( - conv.empty_to_none, - conv.not_none, - conv.test( - lambda declarants: len(declarants) <= 2, - error = N_(u'A "foyer_fiscal" must have at most 2 "declarants"'), - ), - conv.uniform_sequence(conv.pipe( - )), - ), - personnes_a_charge = conv.uniform_sequence( - conv.test( - lambda individu_id: - individu_by_id[individu_id].get('handicap', False) - or find_age(individu_by_id[individu_id], period.start.date, - default = 0) <= 25, - error = u"Une personne à charge d'un foyer fiscal doit avoir moins de" - u" 25 ans ou être handicapée", - ), - ), - ), - default = conv.noop, - ), - ), - conv.empty_to_none, - conv.not_none, - ), - menages = conv.pipe( - conv.uniform_sequence( - conv.struct( - dict( - personne_de_reference = conv.not_none, - ), - default = conv.noop, - ), - ), - conv.empty_to_none, - conv.not_none, - ), - ), - default = conv.noop, - )(test_case, state = state) - - return test_case, error - # First validation and conversion step - test_case, error = conv.pipe( - conv.test_isinstance(dict), - conv.struct( - dict( - foyers_fiscaux = conv.pipe( - conv.make_item_to_singleton(), - conv.test_isinstance(list), - conv.uniform_sequence( - conv.test_isinstance(dict), - drop_none_items = True, - ), - conv.uniform_sequence( - conv.struct( - dict(itertools.chain( - dict( - declarants = conv.pipe( - conv.make_item_to_singleton(), - conv.test_isinstance(list), - conv.uniform_sequence( - conv.test_isinstance((basestring, int)), - drop_none_items = True, - ), - conv.default([]), - ), - id = conv.pipe( - conv.test_isinstance((basestring, int)), - conv.not_none, - ), - personnes_a_charge = conv.pipe( - conv.make_item_to_singleton(), - conv.test_isinstance(list), - conv.uniform_sequence( - conv.test_isinstance((basestring, int)), - drop_none_items = True, - ), - conv.default([]), - ), - ).iteritems(), - )), - drop_none_values = True, - ), - drop_none_items = True, - ), - conv.default([]), - ), - menages = conv.pipe( - conv.make_item_to_singleton(), - conv.test_isinstance(list), - conv.uniform_sequence( - conv.test_isinstance(dict), - drop_none_items = True, - ), - conv.uniform_sequence( - conv.struct( - dict(itertools.chain( - dict( - autres = conv.pipe( - # personnes ayant un lien autre avec la personne de référence - conv.make_item_to_singleton(), - conv.test_isinstance(list), - conv.uniform_sequence( - conv.test_isinstance((basestring, int)), - drop_none_items = True, - ), - conv.default([]), - ), - # conjoint de la personne de référence - conjoint = conv.test_isinstance((basestring, int)), - enfants = conv.pipe( - # enfants de la personne de référence ou de son conjoint - conv.make_item_to_singleton(), - conv.test_isinstance(list), - conv.uniform_sequence( - conv.test_isinstance((basestring, int)), - drop_none_items = True, - ), - conv.default([]), - ), - id = conv.pipe( - conv.test_isinstance((basestring, int)), - conv.not_none, - ), - personne_de_reference = conv.test_isinstance((basestring, int)), - ).iteritems(), - )), - drop_none_values = True, - ), - drop_none_items = True, - ), - conv.default([]), - ), - ), - ), - )(test_case, state = state) - - # test_case, error = conv.struct( - # dict( - # foyers_fiscaux = conv.uniform_sequence( - # conv.struct( - # dict( - # declarants = conv.uniform_sequence(conv.test_in_pop(foyers_fiscaux_individus_id)), - # personnes_a_charge = conv.uniform_sequence(conv.test_in_pop( - # foyers_fiscaux_individus_id)), - # ), - # default = conv.noop, - # ), - # ), - # menages = conv.uniform_sequence( - # conv.struct( - # dict( - # autres = conv.uniform_sequence(conv.test_in_pop(menages_individus_id)), - # conjoint = conv.test_in_pop(menages_individus_id), - # enfants = conv.uniform_sequence(conv.test_in_pop(menages_individus_id)), - # personne_de_reference = conv.test_in_pop(menages_individus_id), - # ), - # default = conv.noop, - # ), - # ), - # ), - # default = conv.noop, - # )(test_case, state = state) - - return test_case, error - - def attribute_groupless_persons_to_entities(self, test_case, period, groupless_individus): - individus_without_menage = groupless_individus['menages'] - individus_without_foyer_fiscal = groupless_individus['foyers_fiscaux'] - - individu_by_id = { - individu['id']: individu - for individu in test_case['individus'] - } - - # Affecte à un foyer fiscal chaque individu qui n'appartient à aucun d'entre eux. - new_foyer_fiscal = dict( - declarants = [], - personnes_a_charge = [], - ) - new_foyer_fiscal_id = None - for individu_id in individus_without_foyer_fiscal[:]: - # Tente d'affecter l'individu à un foyer fiscal d'après son ménage. - menage, menage_role = find_menage_and_role(test_case, individu_id) - if menage_role == u'personne_de_reference': - conjoint_id = menage[u'conjoint'] - if conjoint_id is not None: - foyer_fiscal, other_role = find_foyer_fiscal_and_role(test_case, conjoint_id) - if other_role == u'declarants' and len(foyer_fiscal[u'declarants']) == 1: - # Quand l'individu n'est pas encore dans un foyer fiscal, mais qu'il est personne de - # référence dans un ménage, qu'il y a un conjoint dans ce ménage et que ce - # conjoint est seul déclarant dans un foyer fiscal, alors ajoute l'individu comme - # autre déclarant de ce foyer fiscal. - foyer_fiscal[u'declarants'].append(individu_id) - individus_without_foyer_fiscal.remove(individu_id) - elif menage_role == u'conjoint': - personne_de_reference_id = menage[u'personne_de_reference'] - if personne_de_reference_id is not None: - foyer_fiscal, other_role = find_foyer_fiscal_and_role(test_case, personne_de_reference_id) - if other_role == u'declarants' and len(foyer_fiscal[u'declarants']) == 1: - # Quand l'individu n'est pas encore dans un foyer fiscal, mais qu'il est conjoint - # dans un ménage, qu'il y a une personne de référence dans ce ménage et que - # cette personne est seul déclarant dans un foyer fiscal, alors ajoute l'individu - # comme autre déclarant de ce foyer fiscal. - foyer_fiscal[u'declarants'].append(individu_id) - individus_without_foyer_fiscal.remove(individu_id) - elif menage_role == u'enfants' and ( - menage['personne_de_reference'] is not None or menage[u'conjoint'] is not None): - for other_id in (menage['personne_de_reference'], menage[u'conjoint']): - if other_id is None: - continue - foyer_fiscal, other_role = find_foyer_fiscal_and_role(test_case, other_id) - if other_role == u'declarants': - # Quand l'individu n'est pas encore dans un foyer fiscal, mais qu'il est enfant dans - # un ménage, qu'il y a une personne à charge ou un conjoint dans ce ménage et que - # celui-ci est déclarant dans un foyer fiscal, alors ajoute l'individu comme - # personne à charge de ce foyer fiscal. - foyer_fiscal[u'personnes_a_charge'].append(individu_id) - individus_without_foyer_fiscal.remove(individu_id) - break - - if individu_id in individus_without_foyer_fiscal: - # L'individu n'est toujours pas affecté à un foyer fiscal. - individu = individu_by_id[individu_id] - age = find_age(individu, period.start.date) - if len(new_foyer_fiscal[u'declarants']) < 2 and (age is None or age >= 18): - new_foyer_fiscal[u'declarants'].append(individu_id) - else: - new_foyer_fiscal[u'personnes_a_charge'].append(individu_id) - if new_foyer_fiscal_id is None: - new_foyer_fiscal[u'id'] = new_foyer_fiscal_id = unicode(uuid.uuid4()) - test_case[u'foyers_fiscaux'].append(new_foyer_fiscal) - individus_without_foyer_fiscal.remove(individu_id) - - # Affecte à un ménage chaque individu qui n'appartient à aucun d'entre eux. - new_menage = dict( - autres = [], - conjoint = None, - enfants = [], - personne_de_reference = None, - ) - new_menage_id = None - for individu_id in menages_individus_id[:]: - # Tente d'affecter l'individu à un ménage d'après son foyer fiscal. - foyer_fiscal, foyer_fiscal_role = find_foyer_fiscal_and_role(test_case, individu_id) - if foyer_fiscal_role == u'declarants' and len(foyer_fiscal[u'declarants']) == 2: - for declarant_id in foyer_fiscal[u'declarants']: - if declarant_id != individu_id: - menage, other_role = find_menage_and_role(test_case, declarant_id) - if other_role == u'personne_de_reference' and menage[u'conjoint'] is None: - # Quand l'individu n'est pas encore dans un ménage, mais qu'il est déclarant - # dans un foyer fiscal, qu'il y a un autre déclarant dans ce foyer fiscal et que - # cet autre déclarant est personne de référence dans un ménage et qu'il n'y a - # pas de conjoint dans ce ménage, alors ajoute l'individu comme conjoint de ce - # ménage. - menage[u'conjoint'] = individu_id - menages_individus_id.remove(individu_id) - elif other_role == u'conjoint' and menage[u'personne_de_reference'] is None: - # Quand l'individu n'est pas encore dans un ménage, mais qu'il est déclarant - # dans une foyer fiscal, qu'il y a un autre déclarant dans ce foyer fiscal et - # que cet autre déclarant est conjoint dans un ménage et qu'il n'y a pas de - # personne de référence dans ce ménage, alors ajoute l'individu comme personne - # de référence de ce ménage. - menage[u'personne_de_reference'] = individu_id - menages_individus_id.remove(individu_id) - break - elif foyer_fiscal_role == u'personnes_a_charge' and foyer_fiscal[u'declarants']: - for declarant_id in foyer_fiscal[u'declarants']: - menage, other_role = find_menage_and_role(test_case, declarant_id) - if other_role in (u'personne_de_reference', u'conjoint'): - # Quand l'individu n'est pas encore dans un ménage, mais qu'il est personne à charge - # dans un foyer fiscal, qu'il y a un déclarant dans ce foyer fiscal et que ce - # déclarant est personne de référence ou conjoint dans un ménage, alors ajoute - # l'individu comme enfant de ce ménage. - menage[u'enfants'].append(individu_id) - menages_individus_id.remove(individu_id) - break - - if individu_id in menages_individus_id: - # L'individu n'est toujours pas affecté à un ménage. - if new_menage[u'personne_de_reference'] is None: - new_menage[u'personne_de_reference'] = individu_id - elif new_menage[u'conjoint'] is None: - new_menage[u'conjoint'] = individu_id - else: - new_menage[u'enfants'].append(individu_id) - if new_menage_id is None: - new_menage[u'id'] = new_menage_id = unicode(uuid.uuid4()) - test_case[u'menages'].append(new_menage) - menages_individus_id.remove(individu_id) - - remaining_individus_id = set(individus_without_foyer_fiscal).union(menages_individus_id) - if remaining_individus_id: - individu_index_by_id = { - individu[u'id']: individu_index - for individu_index, individu in enumerate(test_case[u'individus']) - } - if error is None: - error = {} - for individu_id in remaining_individus_id: - error.setdefault('individus', {})[individu_index_by_id[individu_id]] = state._( - u"Individual is missing from {}").format( - state._(u' & ').join( - word - for word in [ - u'foyers_fiscaux' if individu_id in individus_without_foyer_fiscal else None, - u'menages' if individu_id in menages_individus_id else None, - ] - if word is not None - )) - if error is not None: - return test_case, error - - # Third validation step - individu_by_id = test_case['individus'] - test_case, error = conv.struct( - dict( - foyers_fiscaux = conv.pipe( - conv.uniform_sequence( - conv.struct( - dict( - declarants = conv.pipe( - conv.empty_to_none, - conv.not_none, - conv.test( - lambda declarants: len(declarants) <= 2, - error = N_(u'A "foyer_fiscal" must have at most 2 "declarants"'), - ), - # conv.uniform_sequence(conv.pipe( - # conv.test(lambda individu_id: - # find_age(individu_by_id[individu_id], period.start.date, - # default = 100) >= 18, - # error = u"Un déclarant d'un foyer fiscal doit être agé d'au moins 18" - # u" ans", - # ), - # conv.test( - # lambda individu_id: individu_id in parents_id, - # error = u"Un déclarant ou un conjoint sur la déclaration d'impôt, doit" - # u" être un parent dans sa famille", - # ), - # )), - ), - personnes_a_charge = conv.uniform_sequence( - conv.test( - lambda individu_id: - individu_by_id[individu_id].get('invalide', False) - or find_age(individu_by_id[individu_id], period.start.date, - default = 0) < 25, - error = u"Une personne à charge d'un foyer fiscal doit avoir moins de" - u" 25 ans ou être invalide", - ), - ), - ), - default = conv.noop, - ), - ), - conv.empty_to_none, - conv.not_none, - ), - # individus = conv.uniform_sequence( - # conv.struct( - # dict( - # date_naissance = conv.test( - # lambda date_naissance: period.start.date - date_naissance >= datetime.timedelta(0), - # error = u"L'individu doit être né au plus tard le jour de la simulation", - # ), - # ), - # default = conv.noop, - # drop_none_values = 'missing', - # ), - # ), - menages = conv.pipe( - conv.uniform_sequence( - conv.struct( - dict( - personne_de_reference = conv.not_none, - ), - default = conv.noop, - ), - ), - conv.empty_to_none, - conv.not_none, - ), - ), - default = conv.noop, - )(test_case, state = state) - - return test_case, error - - return json_or_python_to_test_case - - def suggest(self): - """Returns a dict of suggestions and modifies self.test_case applying those suggestions.""" - test_case = self.test_case - if test_case is None: - return None - - period_start_date = self.period.start.date - period_start_year = self.period.start.year - suggestions = dict() - - for individu in test_case['individus']: - individu_id = individu['id'] - if ( - individu.get('age') is None and - individu.get('date_naissance') is None - ): - # Add missing date_naissance date to person (a parent is 40 years old and a child is 10 years old. - is_declarant = any( - individu_id in foyer_fiscal['declarants'] - for foyer_fiscal in test_case['foyers_fiscaux'] - ) - date_naissance_year = period_start_year - 40 if is_declarant else period_start_year - 10 - date_naissance = datetime.date(date_naissance_year, 1, 1) - individu['date_naissance'] = date_naissance - suggestions.setdefault('test_case', {}).setdefault('individus', {}).setdefault(individu_id, {})[ - 'date_naissance'] = date_naissance.isoformat() - - return suggestions or None - - def to_json(self): - self_json = collections.OrderedDict() - if self.axes is not None: - self_json['axes'] = self.axes - if self.period is not None: - self_json['period'] = str(self.period) - - test_case = self.test_case - if test_case is not None: - column_by_name = self.tax_benefit_system.column_by_name - test_case_json = collections.OrderedDict() - - foyers_fiscaux_json = [] - for foyer_fiscal in (test_case.get('foyers_fiscaux') or []): - foyer_fiscal_json = collections.OrderedDict() - foyer_fiscal_json['id'] = foyer_fiscal['id'] - declarants = foyer_fiscal.get('declarants') - if declarants: - foyer_fiscal_json['declarants'] = declarants - personnes_a_charge = foyer_fiscal.get('personnes_a_charge') - if personnes_a_charge: - foyer_fiscal_json['personnes_a_charge'] = personnes_a_charge - for column_name, variable_value in foyer_fiscal.iteritems(): - column = column_by_name.get(column_name) - if column is not None and column.entity == FoyerFiscal: - variable_value_json = column.transform_value_to_json(variable_value) - if variable_value_json is not None: - foyer_fiscal_json[column_name] = variable_value_json - foyers_fiscaux_json.append(foyer_fiscal_json) - if foyers_fiscaux_json: - test_case_json['foyers_fiscaux'] = foyers_fiscaux_json - - individus_json = [] - for individu in (test_case.get('individus') or []): - individu_json = collections.OrderedDict() - for column_name, variable_value in individu.iteritems(): - column = column_by_name.get(column_name) - if column is not None and column.entity == Individu: - variable_value_json = column.transform_value_to_json(variable_value) - if variable_value_json is not None: - individu_json[column_name] = variable_value_json - individus_json.append(individu_json) - if individus_json: - test_case_json['individus'] = individus_json - - menages_json = [] - for menage in (test_case.get('menages') or []): - menage_json = collections.OrderedDict() - menage_json['id'] = menage['id'] - personne_de_reference = menage.get('personne_de_reference') - if personne_de_reference is not None: - menage_json['personne_de_reference'] = personne_de_reference - conjoint = menage.get('conjoint') - if conjoint is not None: - menage_json['conjoint'] = conjoint - enfants = menage.get('enfants') - if enfants: - menage_json['enfants'] = enfants - autres = menage.get('autres') - if autres: - menage_json['autres'] = autres - for column_name, variable_value in menage.iteritems(): - column = column_by_name.get(column_name) - if column is not None and column.entity == Menage: - variable_value_json = column.transform_value_to_json(variable_value) - if variable_value_json is not None: - menage_json[column_name] = variable_value_json - menages_json.append(menage_json) - if menages_json: - test_case_json['menages'] = menages_json - - self_json['test_case'] = test_case_json - return self_json - - -# Finders - - -def find_foyer_fiscal_and_role(test_case, individu_id): - for foyer_fiscal in test_case['foyers_fiscaux']: - for role in (u'declarants', u'personnes_a_charge'): - if individu_id in foyer_fiscal[role]: - return foyer_fiscal, role - return None, None - - -def find_menage_and_role(test_case, individu_id): - for menage in test_case['menages']: - for role in (u'personne_de_reference', u'conjoint'): - if menage[role] == individu_id: - return menage, role - for role in (u'enfants', u'autres'): - if individu_id in menage[role]: - return menage, role - return None, None - - -def find_age(individu, date, default = None): - date_naissance = individu.get('date_naissance') - if date_naissance is not None: - age = date.year - date_naissance.year - if date.month < date_naissance.month or date.month == date_naissance.month and date.day < date_naissance.day: - age -= 1 - return age - age = individu.get('age') - if age is not None: - return age - age = individu.get('age') - if age is not None: - return age - agem = individu.get('agem') - if agem is not None: - return agem / 12.0 - return default diff --git a/openfisca_tunisia_pension/scripts/migrations/xml_to_yaml_tunisia.py b/openfisca_tunisia_pension/scripts/migrations/xml_to_yaml_tunisia.py deleted file mode 100644 index e715865..0000000 --- a/openfisca_tunisia_pension/scripts/migrations/xml_to_yaml_tunisia.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- - -''' xml_to_yaml_tunisia.py : Parse XML parameter files for Openfisca-Tunisia and convert them to YAML files. Comments are NOT transformed. - -Usage : - `python xml_to_yaml_tunisia.py output_dir` -or just (output is written in a directory called `yaml_parameters`): - `python xml_to_yaml_tunisia.py` -''' - -import sys -import os - -from openfisca_tunisia_pension.tunisia_pension_taxbenefitsystem import COUNTRY_DIR -from openfisca_core.scripts.migrations.v16_2_to_v17 import xml_to_yaml - - -if len(sys.argv) > 1: - target_path = sys.argv[1] -else: - target_path = os.path.join(COUNTRY_DIR, 'parameters') - -param_dir = os.path.join(COUNTRY_DIR, 'param') -param_files = [ - 'param.xml', - ] -legislation_xml_info_list = [ - (os.path.join(param_dir, param_file), []) - for param_file in param_files -] - -xml_to_yaml.write_parameters(legislation_xml_info_list, target_path) diff --git a/openfisca_tunisia_pension/tests/base.py b/openfisca_tunisia_pension/tests/base.py deleted file mode 100644 index ccdb586..0000000 --- a/openfisca_tunisia_pension/tests/base.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- - - -from openfisca_core.tools import assert_near - -from openfisca_tunisia_pension import TunisiaPensionTaxBenefitSystem - - -__all__ = [ - 'assert_near', - 'tax_benefit_system', - 'TunisiaPensionTaxBenefitSystem', - ] - - -tax_benefit_system = TunisiaPensionTaxBenefitSystem() diff --git a/openfisca_tunisia_pension/tests/formulas/pension_rsna.yaml b/openfisca_tunisia_pension/tests/formulas/pension_rsna.yaml index d7f5bae..543aaf5 100644 --- a/openfisca_tunisia_pension/tests/formulas/pension_rsna.yaml +++ b/openfisca_tunisia_pension/tests/formulas/pension_rsna.yaml @@ -1,7 +1,7 @@ -- name: "Individu salarié 12000 DT par an toute sa carrière" +- name: "Individu salarié 12000 DT par an durant toute sa carrière" period: 2011 absolute_error_margin: 0.5 - input_variables: + input: age: 60 trimestres_valides: 50 salaire: @@ -45,6 +45,6 @@ 2012: 12000 2013: 12000 2014: 12000 - output_variables: + output: salaire_reference_rsna: 12000 - pension_rsna: 5400 \ No newline at end of file + pension_rsna: 5400 diff --git a/openfisca_tunisia_pension/tests/formulas/salaire_reference_rsna.yaml b/openfisca_tunisia_pension/tests/formulas/salaire_reference_rsna.yaml index 678fcf0..a763fd9 100644 --- a/openfisca_tunisia_pension/tests/formulas/salaire_reference_rsna.yaml +++ b/openfisca_tunisia_pension/tests/formulas/salaire_reference_rsna.yaml @@ -1,7 +1,7 @@ - name: "Individu salarié 12000 DT par an toute sa carrière" period: 2011 absolute_error_margin: 0.5 - input_variables: + input: age: 60 trimestres_valides: 50 salaire: @@ -45,5 +45,5 @@ 2012: 12000 2013: 12000 2014: 12000 - output_variables: - salaire_reference_rsna: 12000 \ No newline at end of file + output: + salaire_reference_rsna: 12000 diff --git a/openfisca_tunisia_pension/tests/test_pension.py b/openfisca_tunisia_pension/tests/test_pension.py deleted file mode 100644 index 91a53c3..0000000 --- a/openfisca_tunisia_pension/tests/test_pension.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- - -from openfisca_core import periods -from openfisca_core.tools import assert_near -from openfisca_tunisia_pension.tests import base - - -def test_rsna(): - year = 2011 - simulation = base.tax_benefit_system.new_scenario().init_single_entity( - period = periods.period(year), - parent1 = dict( - age = 60, - trimestres_valides = 50, - salaire = dict( - [("{}".format(yr + 1), 12 * 1000) for yr in range(2014 - 40, 2014)] - ), - ), - ).new_simulation(debug = True) - - assert_near(simulation.calculate_add('salaire_reference_rsna', period = year), 12000, .001) - assert_near(simulation.calculate_add('pension_rsna', period = year), 5400, 1) - - -if __name__ == '__main__': - test_rsna() diff --git a/openfisca_tunisia_pension/tests/test_yaml.py b/openfisca_tunisia_pension/tests/test_yaml.py deleted file mode 100755 index 8489b7d..0000000 --- a/openfisca_tunisia_pension/tests/test_yaml.py +++ /dev/null @@ -1,40 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -import os -from nose.tools import nottest - -from openfisca_core.tools.test_runner import generate_tests - -from openfisca_tunisia_pension.tests import base - -nottest(generate_tests) - -options_by_dir = { - 'formulas': {}, - } - - -def test(): - for directory, options in options_by_dir.iteritems(): - path = os.path.abspath(os.path.join(os.path.dirname(__file__), directory)) - - if options.get('requires'): - # Check if the required package was successfully imported in tests/base.py - if getattr(base, options.get('requires')) is None: - continue - - if not options.get('default_relative_error_margin') and not options.get('default_absolute_error_margin'): - options['default_absolute_error_margin'] = 0.005 - - reform_keys = options.get('reforms') - tax_benefit_system = base.get_cached_composed_reform( - reform_keys = reform_keys, - tax_benefit_system = base.tax_benefit_system, - ) if reform_keys is not None else base.tax_benefit_system - - test_generator = generate_tests(tax_benefit_system, [path], options) - - for test in test_generator: - yield test - diff --git a/openfisca_tunisia_pension/tunisia_pension_taxbenefitsystem.py b/openfisca_tunisia_pension/tunisia_pension_taxbenefitsystem.py index f8720c8..a844e21 100644 --- a/openfisca_tunisia_pension/tunisia_pension_taxbenefitsystem.py +++ b/openfisca_tunisia_pension/tunisia_pension_taxbenefitsystem.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- - import glob import os from openfisca_core.taxbenefitsystems import TaxBenefitSystem -from . import entities, scenarios +from openfisca_tunisia_pension import entities COUNTRY_DIR = os.path.dirname(os.path.abspath(__file__)) EXTENSIONS_PATH = os.path.join(COUNTRY_DIR, 'extensions') @@ -13,15 +11,14 @@ class TunisiaPensionTaxBenefitSystem(TaxBenefitSystem): - """Tunisian pensions tax benefit system""" - CURRENCY = u"DT" + '''Tunisian pensions tax benefit system''' + CURRENCY = 'DT' def __init__(self): super(TunisiaPensionTaxBenefitSystem, self).__init__(entities.entities) - self.Scenario = scenarios.Scenario # We add to our tax and benefit system all the variables - self.add_variables_from_directory(os.path.join(COUNTRY_DIR, 'model')) + self.add_variables_from_directory(os.path.join(COUNTRY_DIR, 'variables')) # We add to our tax and benefit system all the legislation parameters defined in the parameters files parameters_path = os.path.join(COUNTRY_DIR, 'parameters') diff --git a/openfisca_tunisia_pension/model/__init__.py b/openfisca_tunisia_pension/variables/__init__.py similarity index 100% rename from openfisca_tunisia_pension/model/__init__.py rename to openfisca_tunisia_pension/variables/__init__.py diff --git a/openfisca_tunisia_pension/model/data.py b/openfisca_tunisia_pension/variables/data.py similarity index 51% rename from openfisca_tunisia_pension/model/data.py rename to openfisca_tunisia_pension/variables/data.py index a6a2b68..c2942d8 100644 --- a/openfisca_tunisia_pension/model/data.py +++ b/openfisca_tunisia_pension/variables/data.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- - - -from openfisca_tunisia_pension.model.base import * +from openfisca_core.model_api import * +from openfisca_tunisia_pension.entities import Individu # raic -> raci @@ -15,28 +13,28 @@ class date_naissance(Variable): value_type = date default_value = date(1970, 1, 1) entity = Individu - label = u"Date de naissance" + label = 'Date de naissance' definition_period = ETERNITY class salaire(Variable): value_type = float entity = Individu - label = u"Salaires" + label = 'Salaires' definition_period = YEAR class age(Variable): value_type = int entity = Individu - label = u"Âge" + label = 'Âge' definition_period = YEAR class trimestres_valides(Variable): value_type = int entity = Individu - label = u"Nombre de trimestres validés" + label = 'Nombre de trimestres validés' definition_period = YEAR @@ -44,26 +42,25 @@ class TypesRegimeSecuriteSociale(Enum): __order__ = 'rsna rsa rsaa rtns rtte re rtfr raci salarie_cnrps pensionne_cnrps' # Needed to preserve the enum order in Python 2 - rsna = u"Régime des Salariés Non Agricoles" - rsa = u"Régime des Salariés Agricoles" - rsaa = u"Régime des Salariés Agricoles Amélioré" - rtns = u"Régime des Travailleurs Non Salariés (secteurs agricole et non agricole)" - rtte = u"Régime des Travailleurs Tunisiens à l'Etranger" - re = u"Régime des Etudiants, diplômés de l'enseignement supérieur et stagiaires" - rtfr = u"Régime des Travailleurs à Faibles Revenus (gens de maisons, travailleurs de chantiers, et artisans travaillant à la pièce)" - raci = u"Régime des Artistes, Créateurs et Intellectuels" - salarie_cnrps = u"Régime des salariés affilés à la Caisse Nationale de Retraite et de Prévoyance Sociale" - pensionne_cnrps = u"Régime des salariés des pensionnés de la Caisse Nationale de Retraite et de Prévoyance Sociale" + rsna = 'Régime des Salariés Non Agricoles' + rsa = 'Régime des Salariés Agricoles' + rsaa = 'Régime des Salariés Agricoles Amélioré' + rtns = 'Régime des Travailleurs Non Salariés (secteurs agricole et non agricole)' + rtte = "Régime des Travailleurs Tunisiens à l'Etranger" + re = "Régime des Etudiants, diplômés de l'enseignement supérieur et stagiaires" + rtfr = 'Régime des Travailleurs à Faibles Revenus (gens de maisons, travailleurs de chantiers, et artisans travaillant à la pièce)' + raci = 'Régime des Artistes, Créateurs et Intellectuels' + salarie_cnrps = 'Régime des salariés affiliés à la Caisse Nationale de Retraite et de Prévoyance Sociale' + pensionne_cnrps = 'Régime des salariés des pensionnés de la Caisse Nationale de Retraite et de Prévoyance Sociale' # references : # http://www.social.gov.tn/index.php?id=49&L=0 # http://www.paie-tunisie.com/412/fr/83/reglementations/regimes-de-securite-sociale.aspx - class regime_securite_sociale(Variable): value_type = Enum possible_values = TypesRegimeSecuriteSociale default_value = TypesRegimeSecuriteSociale.rsna entity = Individu - label = u"Régime de sécurité sociale du retraité" + label = 'Régime de sécurité sociale du retraité' definition_period = YEAR diff --git a/openfisca_tunisia_pension/model/pension.py b/openfisca_tunisia_pension/variables/pension.py similarity index 78% rename from openfisca_tunisia_pension/model/pension.py rename to openfisca_tunisia_pension/variables/pension.py index 0bcb872..2a07b13 100644 --- a/openfisca_tunisia_pension/model/pension.py +++ b/openfisca_tunisia_pension/variables/pension.py @@ -1,8 +1,6 @@ -# -*- coding:utf-8 -*- - - from __future__ import division + import bottleneck import functools from numpy import ( @@ -14,13 +12,14 @@ ) from openfisca_core import periods -from openfisca_tunisia_pension.model.base import * # noqa +from openfisca_core.model_api import * +from openfisca_tunisia_pension.entities import Individu class salaire_reference_rsa(Variable): value_type = float entity = Individu - label = u"Salaires de référence du régime des salariés agricoles" + label = 'Salaires de référence du régime des salariés agricoles' definition_period = YEAR def formula(individu, period): @@ -36,7 +35,7 @@ def formula(individu, period): mean_over_largest, axis = 0, arr = vstack([ - individu('salaire', period = periods.period("year", year)) + individu('salaire', period = periods.period('year', year)) for year in range(period.start.year, period.start.year - n, -1) ]), ) @@ -47,7 +46,7 @@ def formula(individu, period): class salaire_reference_rsna(Variable): value_type = float entity = Individu - label = u"Salaires de référence du régime des salariés non agricoles" + label = 'Salaires de référence du régime des salariés non agricoles' definition_period = YEAR def formula(individu, period): @@ -69,7 +68,7 @@ def formula(individu, period): class pension_rsna(Variable): value_type = float entity = Individu - label = u"Pension des affiliés au régime des salariés non agricoles" + label = 'Pension des affiliés au régime des salariés non agricoles' definition_period = YEAR def formula(individu, period, parameters): @@ -83,7 +82,7 @@ def formula(individu, period, parameters): age_eligible = parameters(period).pension.rsna.age_dep_anticip periode_remplacement_base = parameters(period).pension.rsna.periode_remplacement_base plaf_taux_pension = parameters(period).pension.rsna.plaf_taux_pension - smig = parameters(period).param_gen.smig_48h + smig = parameters(period).marche_travail.smig_48h pension_min_sup = parameters(period).pension.rsna.pension_minimale.sup pension_min_inf = parameters(period).pension.rsna.pension_minimale.inf @@ -112,19 +111,19 @@ def formula(individu, period, parameters): return eligibilite * montant_pension_percu -def _pension_rsa(trimestres_valides, sal_ref_rsa, regime, age, _P): - """ +def _pension_rsa(trimestres_valides, sal_ref_rsa, regime, age, parameters): + ''' Pension du régime des salariés agricoles - """ - taux_annuite_base = _P.pension.rsa.taux_annuite_base - taux_annuite_supplemetaire = _P.pension.rsa.taux_annuite_supplemetaire - duree_stage = _P.pension.rsa.stage_requis - age_elig = _P.pension.rsa.age_legal - periode_remplacement_base = _P.pension.rsa.periode_remplacement_base - plaf_taux_pension = _P.pension.rsa.plaf_taux_pension - smag = _P.param_gen.smag * 25 + ''' + taux_annuite_base = parameters.pension.rsa.taux_annuite_base + taux_annuite_supplemetaire = parameters.pension.rsa.taux_annuite_supplemetaire + duree_stage = parameters.pension.rsa.stage_requis + age_elig = parameters.pension.rsa.age_legal + periode_remplacement_base = parameters.pension.rsa.periode_remplacement_base + plaf_taux_pension = parameters.pension.rsa.plaf_taux_pension + smag = parameters.marche_travail.smag * 25 stage = trimestres_valides > 4 * duree_stage - pension_min = _P.pension.rsa.pension_min + pension_min = parameters.pension.rsa.pension_min sal_ref = sal_ref_rsa montant = pension_generique(trimestres_valides, sal_ref, age, taux_annuite_base, taux_annuite_supplemetaire, @@ -142,10 +141,10 @@ def _pension_rsa(trimestres_valides, sal_ref_rsa, regime, age, _P): def pension_generique(trimestres_valides, sal_ref, age, taux_annuite_base, taux_annuite_supplemetaire, duree_stage, age_elig, periode_remplacement_base, plaf_taux_pension, smig): taux_pension = ( - (trimestres_valides < 4 * periode_remplacement_base) * (trimestres_valides / 4) * taux_annuite_base + - (trimestres_valides >= 4 * periode_remplacement_base) * ( - taux_annuite_base * periode_remplacement_base + - (trimestres_valides / 4 - periode_remplacement_base) * taux_annuite_supplemetaire + (trimestres_valides < 4 * periode_remplacement_base) * (trimestres_valides / 4) * taux_annuite_base + + (trimestres_valides >= 4 * periode_remplacement_base) * ( + taux_annuite_base * periode_remplacement_base + + (trimestres_valides / 4 - periode_remplacement_base) * taux_annuite_supplemetaire ) ) montant = min_(taux_pension, plaf_taux_pension) * sal_ref diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..641d611 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,88 @@ +[project] +name = "OpenFisca-Tunisia-Pension" +version = "2.0.0" +description = "OpenFisca Rules as Code model for Tunisia pensions." +readme = "README.md" +keywords = ["microsimulation", "tax", "benefit", "pension", "rac", "rules-as-code", "tunisia"] +authors = [ + {name = "OpenFisca Team", email = "contact@openfisca.org"}, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Operating System :: POSIX", + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Scientific/Engineering :: Information Analysis", +] +requires-python = ">= 3.9" +dependencies = [ + 'bottleneck >=1.3.2,<=2.0.0', + 'numpy >=1.24.3, <2', + 'openfisca-core[web-api] >=43, <44', + 'scipy >= 0.12', +] + +[project.urls] +Homepage = "https://github.com/openfisca/openfisca-tunisia-pension" +Repository = "https://github.com/openfisca/openfisca-tunisia-pension" +Documentation = "https://openfisca.org/doc" +Issues = "https://github.com/openfisca/openfisca-tunisia-pension/issues" +Changelog = "https://github.com/openfisca/openfisca-tunisia-pension/blob/main/CHANGELOG.md" + +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project.optional-dependencies] +dev = [ + "autopep8 >=2.0.2, <3.0", + "Flake8-pyproject>=1.2.3, <2.0.0", # To read flake8 configuration from pyproject.toml + "flake8 >=6.0.0, <7.0.0", + "flake8-print >=5.0.0, <6.0.0", + "flake8-quotes >=3.3.2", + "pytest", # Let OpenFisca-Core decide pytest version + "scipy >=1.10.1, <2.0", + "requests >=2.28.2, <3.0", + "yamllint >=1.30.0, <2.0" +] +notebook = [ + 'ipykernel >= 4.8', + 'jupyter-client >= 5.2', + 'matplotlib >= 2.2', + 'nbconvert >= 5.3', + 'nbformat >= 4.4', + 'pandas >= 0.22.0', +] + +[tool.flake8] +# ; E128/133: We prefer hang-closing visual indents +# ; E251: We prefer `function(x = 1)` over `function(x=1)` +# ; E501: We do not enforce a maximum line length +# ; F403/405: We ignore * imports +# ; W503/504: We break lines before binary operators (Knuth's style) +hang-closing = true +ignore = ["E128","E251","F403","F405","E501","W503"] +docstring-quotes = "single" +inline-quotes = "single" +multiline-quotes = "single" + +[tool.pep8] +hang-closing = true +ignore = ["E128","E251","F403","F405","E501","W503"] +in-place = true + +[tool.pytest.ini_options] +addopts = "--showlocals --exitfirst --doctest-modules --disable-pytest-warnings" +testpaths = "tests" +python_files = "**/*.py" +filterwarnings = [ + "error", + "ignore::UserWarning", + 'ignore:function ham\(\) is deprecated:DeprecationWarning', + "ignore:invalid value encountered in divide:RuntimeWarning", + "ignore:invalid value encountered in multiply:RuntimeWarning", + "ignore:divide by zero encountered in divide:RuntimeWarning", +] diff --git a/setup.cfg b/setup.cfg index 9a7e54a..ffef11b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,16 +1,22 @@ -# Flake8 +; E128/133: We prefer hang-closing visual indents +; E251: We prefer `function(x = 1)` over `function(x=1)` +; E501: We do not enforce a maximum line length +; F403/405: We ignore * imports +; W503/504: We break lines before binary operators (Knuth's style) [flake8] hang-closing = true -; E128 continuation line under-indented for visual indent -; E251 unexpected spaces around keyword / parameter equals -;F403:'from openfisca_tunisia_pension.model.base import *' used; unable to detect undefined names -;F405:IntCol may be undefined, or defined from star imports: openfisca_tunisia_pension.model.base -ignore = E128,E251,F403,F405 -;max-complexity = 10 -max-line-length = 120 +ignore = E128,E251,F403,F405,E501,W503 +docstring-quotes = single +inline-quotes = single +multiline-quotes = single -# Nose +[pep8] +hang-closing = true +ignore = E128,E251,F403,F405,E501,W503 +in-place = true -[nosetests] -with-doctest = 1 +[tool:pytest] +addopts = --showlocals --exitfirst --doctest-modules --disable-pytest-warnings +testpaths = tests +python_files = **/*.py diff --git a/setup.py b/setup.py deleted file mode 100755 index 9d871d1..0000000 --- a/setup.py +++ /dev/null @@ -1,50 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - - -"""Tunisia Pension specific model for OpenFisca -- a versatile microsimulation free software""" - - -from setuptools import setup, find_packages - - -classifiers = """\ -Development Status :: 2 - Pre-Alpha -License :: OSI Approved :: GNU Affero General Public License v3 -Operating System :: POSIX -Programming Language :: Python -Topic :: Scientific/Engineering :: Information Analysis -""" - -doc_lines = __doc__.split('\n') - - -setup( - name = 'OpenFisca-Tunisia-Pension', - version = '2.0.0', - author = 'OpenFisca Team', - author_email = 'contact@openfisca.org', - classifiers = [classifier for classifier in classifiers.split('\n') if classifier], - description = doc_lines[0], - keywords = 'benefit microsimulation pension social tax tunisia', - license = 'http://www.fsf.org/licensing/licenses/agpl-3.0.html', - long_description = '\n'.join(doc_lines[2:]), - url = 'https://github.com/openfisca/openfisca-tunisia-pension', - - data_files = [], - extras_require = dict( - tests = [ - 'nose', - ], - ), - install_requires = [ - 'Babel >= 0.9.4', - 'numpy<1.15,>=1.11', # Attune Bottleneck & Core dependency - 'Bottleneck == 1.2.0', - 'Biryani[datetimeconv] >= 0.10.4', - 'OpenFisca-Core >= 24.0, < 25.0', - 'PyYAML >= 3.10', - 'scipy >= 0.12', - ], - packages = find_packages(exclude=['openfisca_tunisia_pension.tests*']), - ) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/formulas/pension_rsna.yaml b/tests/formulas/pension_rsna.yaml new file mode 100644 index 0000000..543aaf5 --- /dev/null +++ b/tests/formulas/pension_rsna.yaml @@ -0,0 +1,50 @@ +- name: "Individu salarié 12000 DT par an durant toute sa carrière" + period: 2011 + absolute_error_margin: 0.5 + input: + age: 60 + trimestres_valides: 50 + salaire: + 1975: 12000 + 1976: 12000 + 1977: 12000 + 1978: 12000 + 1979: 12000 + 1980: 12000 + 1981: 12000 + 1982: 12000 + 1983: 12000 + 1984: 12000 + 1985: 12000 + 1986: 12000 + 1987: 12000 + 1988: 12000 + 1989: 12000 + 1990: 12000 + 1991: 12000 + 1992: 12000 + 1993: 12000 + 1994: 12000 + 1995: 12000 + 1996: 12000 + 1997: 12000 + 1998: 12000 + 1999: 12000 + 2000: 12000 + 2001: 12000 + 2002: 12000 + 2003: 12000 + 2004: 12000 + 2005: 12000 + 2006: 12000 + 2007: 12000 + 2008: 12000 + 2009: 12000 + 2010: 12000 + 2011: 12000 + 2012: 12000 + 2013: 12000 + 2014: 12000 + output: + salaire_reference_rsna: 12000 + pension_rsna: 5400 diff --git a/tests/formulas/salaire_reference_rsna.yaml b/tests/formulas/salaire_reference_rsna.yaml new file mode 100644 index 0000000..a763fd9 --- /dev/null +++ b/tests/formulas/salaire_reference_rsna.yaml @@ -0,0 +1,49 @@ +- name: "Individu salarié 12000 DT par an toute sa carrière" + period: 2011 + absolute_error_margin: 0.5 + input: + age: 60 + trimestres_valides: 50 + salaire: + 1975: 12000 + 1976: 12000 + 1977: 12000 + 1978: 12000 + 1979: 12000 + 1980: 12000 + 1981: 12000 + 1982: 12000 + 1983: 12000 + 1984: 12000 + 1985: 12000 + 1986: 12000 + 1987: 12000 + 1988: 12000 + 1989: 12000 + 1990: 12000 + 1991: 12000 + 1992: 12000 + 1993: 12000 + 1994: 12000 + 1995: 12000 + 1996: 12000 + 1997: 12000 + 1998: 12000 + 1999: 12000 + 2000: 12000 + 2001: 12000 + 2002: 12000 + 2003: 12000 + 2004: 12000 + 2005: 12000 + 2006: 12000 + 2007: 12000 + 2008: 12000 + 2009: 12000 + 2010: 12000 + 2011: 12000 + 2012: 12000 + 2013: 12000 + 2014: 12000 + output: + salaire_reference_rsna: 12000