policyengine 3.2.1__tar.gz → 3.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- policyengine-3.2.4/.github/check-changelog.sh +10 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/workflows/pr_code_changes.yaml +9 -2
- policyengine-3.2.4/.github/workflows/push.yaml +160 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/CHANGELOG.md +25 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/PKG-INFO +1 -1
- {policyengine-3.2.1 → policyengine-3.2.4}/pyproject.toml +1 -1
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/__init__.py +8 -0
- policyengine-3.2.4/src/policyengine/core/parameter_node.py +29 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/region.py +9 -1
- policyengine-3.2.4/src/policyengine/core/scoping_strategy.py +224 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/simulation.py +28 -2
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/tax_benefit_model_version.py +20 -2
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/variable.py +3 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/countries/uk/regions.py +26 -4
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/countries/us/regions.py +5 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/uk/model.py +62 -2
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/us/model.py +65 -2
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine.egg-info/PKG-INFO +1 -1
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine.egg-info/SOURCES.txt +8 -4
- policyengine-3.2.4/tests/fixtures/variable_label_fixtures.py +53 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_filtering.py +19 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_models.py +97 -0
- policyengine-3.2.4/tests/test_scoping_strategy.py +339 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_uk_regions.py +14 -0
- policyengine-3.2.4/tests/test_variable_labels.py +190 -0
- policyengine-3.2.1/.github/workflows/code_changes.yaml +0 -60
- policyengine-3.2.1/.github/workflows/docs.yml +0 -49
- policyengine-3.2.1/.github/workflows/versioning.yaml +0 -75
- {policyengine-3.2.1 → policyengine-3.2.4}/.claude/policyengine-guide.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.claude/quick-reference.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/CONTRIBUTING.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/bump_version.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/changelog_template.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/fetch_version.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/get-changelog-diff.sh +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/has-functional-changes.sh +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/is-version-number-acceptable.sh +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/publish-git-tag.sh +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.github/workflows/pr_docs_changes.yaml +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.gitignore +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/.python-version +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/LICENSE +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/Makefile +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/README.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/changelog.d/.gitkeep +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/docs/.gitignore +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/docs/core-concepts.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/docs/country-models-uk.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/docs/country-models-us.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/docs/dev.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/docs/index.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/docs/myst.yml +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/docs/visualisation.md +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/examples/employment_income_variation_uk.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/examples/employment_income_variation_us.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/examples/household_impact_example.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/examples/income_bands_uk.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/examples/income_distribution_us.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/examples/policy_change_uk.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/examples/speedtest_us_simulation.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/setup.cfg +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/cache.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/dataset.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/dataset_version.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/dynamic.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/output.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/parameter.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/parameter_value.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/policy.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/tax_benefit_model.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/countries/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/countries/uk/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/countries/us/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/countries/us/data/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/countries/us/data/districts.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/countries/us/data/places.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/countries/us/data/states.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/aggregate.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/change_aggregate.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/congressional_district_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/constituency_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/decile_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/inequality.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/intra_decile_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/local_authority_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/outputs/poverty.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/uk/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/uk/analysis.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/uk/datasets.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/uk/outputs.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/uk.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/us/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/us/analysis.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/us/datasets.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/us/outputs.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/tax_benefit_models/us.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/utils/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/utils/dates.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/utils/entity_utils.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/utils/parameter_labels.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/utils/parametric_reforms.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/utils/plotting.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine.egg-info/dependency_links.txt +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine.egg-info/requires.txt +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine.egg-info/top_level.txt +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/conftest.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/fixtures/__init__.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/fixtures/filtering_fixtures.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/fixtures/parameter_labels_fixtures.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/fixtures/parametric_reforms_fixtures.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/fixtures/poverty_by_demographics_fixtures.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/fixtures/region_fixtures.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/fixtures/us_reform_fixtures.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_aggregate.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_cache.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_change_aggregate.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_congressional_district_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_constituency_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_entity_mapping.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_entity_utils.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_household_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_inequality.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_intra_decile_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_local_authority_impact.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_pandas3_compatibility.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_parameter_labels.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_parametric_reforms.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_poverty.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_poverty_by_demographics.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_poverty_run.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_region.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_us_reform_application.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/tests/test_us_regions.py +0 -0
- {policyengine-3.2.1 → policyengine-3.2.4}/uv.lock +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
FRAGMENTS=$(find changelog.d -type f ! -name '.gitkeep' | wc -l)
|
|
5
|
+
if [ "$FRAGMENTS" -eq 0 ]; then
|
|
6
|
+
echo "::error::No changelog fragment found in changelog.d/"
|
|
7
|
+
echo "Add one with: echo 'Description.' > changelog.d/\$(git branch --show-current).<type>.md"
|
|
8
|
+
echo "Types: added, changed, fixed, removed, breaking"
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
@@ -7,14 +7,21 @@ on:
|
|
|
7
7
|
- src/**
|
|
8
8
|
- tests/**
|
|
9
9
|
- .github/**
|
|
10
|
+
- changelog.d/**
|
|
10
11
|
workflow_dispatch:
|
|
11
12
|
|
|
12
13
|
jobs:
|
|
14
|
+
check-changelog:
|
|
15
|
+
name: Check changelog fragment
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- name: Check for changelog fragment
|
|
20
|
+
run: .github/check-changelog.sh
|
|
13
21
|
Lint:
|
|
14
22
|
runs-on: ubuntu-latest
|
|
15
23
|
steps:
|
|
16
24
|
- uses: actions/checkout@v4
|
|
17
|
-
# Check formatting with ruff
|
|
18
25
|
- name: Install ruff
|
|
19
26
|
run: pip install ruff
|
|
20
27
|
|
|
@@ -62,4 +69,4 @@ jobs:
|
|
|
62
69
|
- name: Run tests with coverage
|
|
63
70
|
run: make test
|
|
64
71
|
env:
|
|
65
|
-
HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}
|
|
72
|
+
HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Unified workflow for pushes to main.
|
|
2
|
+
#
|
|
3
|
+
# Phase 1 (normal push): Lint + Test → Docs + Versioning
|
|
4
|
+
# Versioning commits with message "Update package version".
|
|
5
|
+
# The commit pushes to main, re-triggering this workflow for Phase 2.
|
|
6
|
+
#
|
|
7
|
+
# Phase 2 (sentinel commit): Publish to PyPI + GitHub Release
|
|
8
|
+
# Skips Lint + Test since the code is identical to Phase 1.
|
|
9
|
+
|
|
10
|
+
name: Push to main
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches:
|
|
15
|
+
- main
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write
|
|
20
|
+
pages: write
|
|
21
|
+
id-token: write
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
# ── Phase 1 gates (skip on sentinel commit) ─────────────────
|
|
25
|
+
Lint:
|
|
26
|
+
if: github.event.head_commit.message != 'Update package version'
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- name: Install ruff
|
|
31
|
+
run: pip install ruff
|
|
32
|
+
- name: Run ruff format check
|
|
33
|
+
run: ruff format --check .
|
|
34
|
+
- name: Run ruff check
|
|
35
|
+
run: ruff check .
|
|
36
|
+
|
|
37
|
+
Test:
|
|
38
|
+
if: github.event.head_commit.message != 'Update package version'
|
|
39
|
+
runs-on: macos-latest
|
|
40
|
+
strategy:
|
|
41
|
+
fail-fast: false
|
|
42
|
+
matrix:
|
|
43
|
+
python-version: ['3.13', '3.14']
|
|
44
|
+
steps:
|
|
45
|
+
- name: Checkout repo
|
|
46
|
+
uses: actions/checkout@v4
|
|
47
|
+
- name: Install uv
|
|
48
|
+
uses: astral-sh/setup-uv@v5
|
|
49
|
+
- name: Set up Python
|
|
50
|
+
uses: actions/setup-python@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: ${{ matrix.python-version }}
|
|
53
|
+
allow-prereleases: true
|
|
54
|
+
- name: Install package
|
|
55
|
+
run: uv pip install -e .[dev] --system
|
|
56
|
+
- name: Install policyengine
|
|
57
|
+
run: uv pip install policyengine --system
|
|
58
|
+
- name: Run tests with coverage
|
|
59
|
+
run: make test
|
|
60
|
+
env:
|
|
61
|
+
HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}
|
|
62
|
+
|
|
63
|
+
# ── Phase 1: Docs + Versioning (skip on sentinel commit) ──
|
|
64
|
+
Docs:
|
|
65
|
+
name: Deploy documentation
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
needs: [Lint, Test]
|
|
68
|
+
if: github.event.head_commit.message != 'Update package version'
|
|
69
|
+
concurrency:
|
|
70
|
+
group: 'pages'
|
|
71
|
+
cancel-in-progress: false
|
|
72
|
+
environment:
|
|
73
|
+
name: github-pages
|
|
74
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
75
|
+
env:
|
|
76
|
+
BASE_URL: /${{ github.event.repository.name }}
|
|
77
|
+
steps:
|
|
78
|
+
- uses: actions/checkout@v4
|
|
79
|
+
- name: Setup Pages
|
|
80
|
+
uses: actions/configure-pages@v3
|
|
81
|
+
- uses: actions/setup-node@v4
|
|
82
|
+
with:
|
|
83
|
+
node-version: 18.x
|
|
84
|
+
- name: Install MyST
|
|
85
|
+
run: npm install -g mystmd
|
|
86
|
+
- name: Build HTML Assets
|
|
87
|
+
run: cd docs && myst build --html
|
|
88
|
+
- name: Upload artifact
|
|
89
|
+
uses: actions/upload-pages-artifact@v3
|
|
90
|
+
with:
|
|
91
|
+
path: './docs/_build/html'
|
|
92
|
+
- name: Deploy to GitHub Pages
|
|
93
|
+
id: deployment
|
|
94
|
+
uses: actions/deploy-pages@v4
|
|
95
|
+
|
|
96
|
+
Versioning:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
needs: [Lint, Test]
|
|
99
|
+
if: github.event.head_commit.message != 'Update package version'
|
|
100
|
+
steps:
|
|
101
|
+
- name: Generate GitHub App token
|
|
102
|
+
id: app-token
|
|
103
|
+
uses: actions/create-github-app-token@v1
|
|
104
|
+
with:
|
|
105
|
+
app-id: ${{ secrets.APP_ID }}
|
|
106
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
107
|
+
- name: Checkout repo
|
|
108
|
+
uses: actions/checkout@v4
|
|
109
|
+
with:
|
|
110
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
111
|
+
- name: Setup Python
|
|
112
|
+
uses: actions/setup-python@v5
|
|
113
|
+
with:
|
|
114
|
+
python-version: '3.13'
|
|
115
|
+
- name: Build changelog
|
|
116
|
+
run: pip install yaml-changelog towncrier && make changelog
|
|
117
|
+
- name: Preview changelog update
|
|
118
|
+
run: ".github/get-changelog-diff.sh"
|
|
119
|
+
- name: Update changelog
|
|
120
|
+
uses: EndBug/add-and-commit@v9
|
|
121
|
+
with:
|
|
122
|
+
add: "."
|
|
123
|
+
message: Update package version
|
|
124
|
+
|
|
125
|
+
# ── Phase 2: Publish (only on sentinel commit) ────────────
|
|
126
|
+
Publish:
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
if: github.event.head_commit.message == 'Update package version'
|
|
129
|
+
env:
|
|
130
|
+
GH_TOKEN: ${{ github.token }}
|
|
131
|
+
steps:
|
|
132
|
+
- name: Checkout repo
|
|
133
|
+
uses: actions/checkout@v4
|
|
134
|
+
- name: Install uv
|
|
135
|
+
uses: astral-sh/setup-uv@v5
|
|
136
|
+
- name: Set up Python
|
|
137
|
+
uses: actions/setup-python@v5
|
|
138
|
+
with:
|
|
139
|
+
python-version: '3.13'
|
|
140
|
+
- name: Install package
|
|
141
|
+
run: uv pip install -e .[dev] --system
|
|
142
|
+
- name: Install policyengine
|
|
143
|
+
run: uv pip install policyengine --system
|
|
144
|
+
- name: Publish a git tag
|
|
145
|
+
run: ".github/publish-git-tag.sh"
|
|
146
|
+
- name: Build package
|
|
147
|
+
run: python -m build
|
|
148
|
+
- name: Publish a Python distribution to PyPI
|
|
149
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
150
|
+
with:
|
|
151
|
+
user: __token__
|
|
152
|
+
password: ${{ secrets.PYPI }}
|
|
153
|
+
skip_existing: true
|
|
154
|
+
- name: Create GitHub Release
|
|
155
|
+
run: |
|
|
156
|
+
VERSION=$(python .github/fetch_version.py)
|
|
157
|
+
gh release create "$VERSION" \
|
|
158
|
+
--title "v$VERSION" \
|
|
159
|
+
--notes "See [CHANGELOG.md](https://github.com/PolicyEngine/policyengine.py/blob/main/CHANGELOG.md) for details." \
|
|
160
|
+
--latest
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
## [3.2.4] - 2026-03-17
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- Skip redundant Lint and Test in Phase 2 of push workflow since code is identical to Phase 1
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## [3.2.3] - 2026-03-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Use GitHub App token in push workflow Versioning job to enable auto-triggering of Phase 2 (Publish)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## [3.2.2] - 2026-03-17
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Consolidate CI/CD workflows into a unified push workflow with two-phase sentinel pattern, enforce changelog fragments on PRs
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Use GITHUB_TOKEN instead of missing POLICYENGINE_GITHUB PAT in push workflow
|
|
24
|
+
|
|
25
|
+
|
|
1
26
|
## [3.2.1] - 2026-03-10
|
|
2
27
|
|
|
3
28
|
### Changed
|
|
@@ -6,11 +6,18 @@ from .dynamic import Dynamic as Dynamic
|
|
|
6
6
|
from .output import Output as Output
|
|
7
7
|
from .output import OutputCollection as OutputCollection
|
|
8
8
|
from .parameter import Parameter as Parameter
|
|
9
|
+
from .parameter_node import ParameterNode as ParameterNode
|
|
9
10
|
from .parameter_value import ParameterValue as ParameterValue
|
|
10
11
|
from .policy import Policy as Policy
|
|
11
12
|
from .region import Region as Region
|
|
12
13
|
from .region import RegionRegistry as RegionRegistry
|
|
13
14
|
from .region import RegionType as RegionType
|
|
15
|
+
from .scoping_strategy import RegionScopingStrategy as RegionScopingStrategy
|
|
16
|
+
from .scoping_strategy import RowFilterStrategy as RowFilterStrategy
|
|
17
|
+
from .scoping_strategy import ScopingStrategy as ScopingStrategy
|
|
18
|
+
from .scoping_strategy import (
|
|
19
|
+
WeightReplacementStrategy as WeightReplacementStrategy,
|
|
20
|
+
)
|
|
14
21
|
from .simulation import Simulation as Simulation
|
|
15
22
|
from .tax_benefit_model import TaxBenefitModel as TaxBenefitModel
|
|
16
23
|
from .tax_benefit_model_version import (
|
|
@@ -23,4 +30,5 @@ Dataset.model_rebuild()
|
|
|
23
30
|
TaxBenefitModelVersion.model_rebuild()
|
|
24
31
|
Variable.model_rebuild()
|
|
25
32
|
Parameter.model_rebuild()
|
|
33
|
+
ParameterNode.model_rebuild()
|
|
26
34
|
ParameterValue.model_rebuild()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .tax_benefit_model_version import TaxBenefitModelVersion
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ParameterNode(BaseModel):
|
|
11
|
+
"""Represents a folder/category node in the parameter hierarchy.
|
|
12
|
+
|
|
13
|
+
Parameter nodes are intermediate nodes in the parameter tree (e.g., "gov",
|
|
14
|
+
"gov.hmrc", "gov.hmrc.income_tax"). They provide structure and human-readable
|
|
15
|
+
labels for navigating the parameter tree, but don't have values themselves.
|
|
16
|
+
|
|
17
|
+
Unlike Parameter objects (which are leaf nodes with actual values),
|
|
18
|
+
ParameterNode objects are purely organizational.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
22
|
+
|
|
23
|
+
id: str = Field(default_factory=lambda: str(uuid4()))
|
|
24
|
+
name: str = Field(description="Full path of the node (e.g., 'gov.hmrc')")
|
|
25
|
+
label: str | None = Field(
|
|
26
|
+
default=None, description="Human-readable label (e.g., 'HMRC')"
|
|
27
|
+
)
|
|
28
|
+
description: str | None = Field(default=None, description="Node description")
|
|
29
|
+
tax_benefit_model_version: "TaxBenefitModelVersion"
|
|
@@ -10,6 +10,8 @@ from typing import Literal
|
|
|
10
10
|
|
|
11
11
|
from pydantic import BaseModel, Field, PrivateAttr
|
|
12
12
|
|
|
13
|
+
from .scoping_strategy import ScopingStrategy
|
|
14
|
+
|
|
13
15
|
# Region type literals for US and UK
|
|
14
16
|
USRegionType = Literal["national", "state", "congressional_district", "place"]
|
|
15
17
|
UKRegionType = Literal["national", "country", "constituency", "local_authority"]
|
|
@@ -55,7 +57,13 @@ class Region(BaseModel):
|
|
|
55
57
|
description="GCS path to dedicated dataset (e.g., 'gs://policyengine-us-data/states/CA.h5')",
|
|
56
58
|
)
|
|
57
59
|
|
|
58
|
-
#
|
|
60
|
+
# Scoping strategy (preferred over legacy filter fields)
|
|
61
|
+
scoping_strategy: ScopingStrategy | None = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
description="Strategy for scoping dataset to this region (row filtering or weight replacement)",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Legacy filtering configuration (kept for backward compatibility)
|
|
59
67
|
requires_filter: bool = Field(
|
|
60
68
|
default=False,
|
|
61
69
|
description="True if this region filters from a parent dataset rather than having its own",
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Region scoping strategies for geographic simulations.
|
|
2
|
+
|
|
3
|
+
Provides two concrete strategies for scoping datasets to sub-national regions:
|
|
4
|
+
|
|
5
|
+
1. RowFilterStrategy: Filters dataset rows where a household variable matches
|
|
6
|
+
a specific value (e.g., UK countries by 'country' field, US places by 'place_fips').
|
|
7
|
+
|
|
8
|
+
2. WeightReplacementStrategy: Replaces household weights from a pre-computed weight
|
|
9
|
+
matrix stored in GCS (e.g., UK constituencies and local authorities).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from abc import abstractmethod
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Annotated, Literal
|
|
16
|
+
|
|
17
|
+
import h5py
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
from microdf import MicroDataFrame
|
|
21
|
+
from pydantic import BaseModel, Discriminator
|
|
22
|
+
|
|
23
|
+
from policyengine.utils.entity_utils import (
|
|
24
|
+
filter_dataset_by_household_variable,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RegionScopingStrategy(BaseModel):
|
|
31
|
+
"""Base class for region scoping strategies.
|
|
32
|
+
|
|
33
|
+
Subclasses implement apply() to scope a dataset's entity data
|
|
34
|
+
to a specific sub-national region.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
strategy_type: str
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def apply(
|
|
41
|
+
self,
|
|
42
|
+
entity_data: dict[str, MicroDataFrame],
|
|
43
|
+
group_entities: list[str],
|
|
44
|
+
year: int,
|
|
45
|
+
) -> dict[str, MicroDataFrame]:
|
|
46
|
+
"""Apply the scoping strategy to entity data.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
entity_data: Dict mapping entity names to their MicroDataFrames.
|
|
50
|
+
group_entities: List of group entity names for this country.
|
|
51
|
+
year: The simulation year (used for time-indexed weight matrices).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A dict mapping entity names to scoped MicroDataFrames.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def cache_key(self) -> str:
|
|
59
|
+
"""Return a string key for deterministic simulation ID hashing."""
|
|
60
|
+
return f"{self.strategy_type}:{self.model_dump_json()}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RowFilterStrategy(RegionScopingStrategy):
|
|
64
|
+
"""Scoping strategy that filters dataset rows by a household variable.
|
|
65
|
+
|
|
66
|
+
Used for regions where we want to keep only households matching a
|
|
67
|
+
specific variable value (e.g., UK countries, US places/cities).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
strategy_type: Literal["row_filter"] = "row_filter"
|
|
71
|
+
variable_name: str
|
|
72
|
+
variable_value: str
|
|
73
|
+
|
|
74
|
+
def apply(
|
|
75
|
+
self,
|
|
76
|
+
entity_data: dict[str, MicroDataFrame],
|
|
77
|
+
group_entities: list[str],
|
|
78
|
+
year: int,
|
|
79
|
+
) -> dict[str, MicroDataFrame]:
|
|
80
|
+
return filter_dataset_by_household_variable(
|
|
81
|
+
entity_data=entity_data,
|
|
82
|
+
group_entities=group_entities,
|
|
83
|
+
variable_name=self.variable_name,
|
|
84
|
+
variable_value=self.variable_value,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def cache_key(self) -> str:
|
|
89
|
+
return f"row_filter:{self.variable_name}={self.variable_value}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class WeightReplacementStrategy(RegionScopingStrategy):
|
|
93
|
+
"""Scoping strategy that replaces household weights from a pre-computed matrix.
|
|
94
|
+
|
|
95
|
+
Used for UK constituencies and local authorities. Instead of removing
|
|
96
|
+
households, this strategy keeps all households but replaces their weights
|
|
97
|
+
with region-specific values from a weight matrix stored in GCS.
|
|
98
|
+
|
|
99
|
+
The weight matrix is an HDF5 file with shape (N_regions x N_households),
|
|
100
|
+
where each row contains household weights for a specific region.
|
|
101
|
+
A companion CSV maps region codes/names to row indices.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
strategy_type: Literal["weight_replacement"] = "weight_replacement"
|
|
105
|
+
weight_matrix_bucket: str
|
|
106
|
+
weight_matrix_key: str
|
|
107
|
+
lookup_csv_bucket: str
|
|
108
|
+
lookup_csv_key: str
|
|
109
|
+
region_code: str
|
|
110
|
+
|
|
111
|
+
def apply(
|
|
112
|
+
self,
|
|
113
|
+
entity_data: dict[str, MicroDataFrame],
|
|
114
|
+
group_entities: list[str],
|
|
115
|
+
year: int,
|
|
116
|
+
) -> dict[str, MicroDataFrame]:
|
|
117
|
+
from policyengine_core.tools.google_cloud import download_gcs_file
|
|
118
|
+
|
|
119
|
+
# Download lookup CSV and find region index
|
|
120
|
+
lookup_path = Path(
|
|
121
|
+
download_gcs_file(
|
|
122
|
+
bucket=self.lookup_csv_bucket,
|
|
123
|
+
file_path=self.lookup_csv_key,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
lookup_df = pd.read_csv(lookup_path)
|
|
127
|
+
|
|
128
|
+
region_id = self._find_region_index(lookup_df, self.region_code)
|
|
129
|
+
|
|
130
|
+
# Download weight matrix and extract weights for this region
|
|
131
|
+
weights_path = download_gcs_file(
|
|
132
|
+
bucket=self.weight_matrix_bucket,
|
|
133
|
+
file_path=self.weight_matrix_key,
|
|
134
|
+
)
|
|
135
|
+
with h5py.File(weights_path, "r") as f:
|
|
136
|
+
weights = f[str(year)][...]
|
|
137
|
+
|
|
138
|
+
region_weights = weights[region_id]
|
|
139
|
+
|
|
140
|
+
# Validate weight row length matches household count
|
|
141
|
+
household_df = pd.DataFrame(entity_data["household"])
|
|
142
|
+
if len(region_weights) != len(household_df):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"Weight matrix row length ({len(region_weights)}) does not match "
|
|
145
|
+
f"household count ({len(household_df)}) for region '{self.region_code}'. "
|
|
146
|
+
f"The weight matrix may be out of date."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Replace household weights
|
|
150
|
+
result = {}
|
|
151
|
+
for entity_name, mdf in entity_data.items():
|
|
152
|
+
df = pd.DataFrame(mdf)
|
|
153
|
+
if entity_name == "household":
|
|
154
|
+
df["household_weight"] = region_weights
|
|
155
|
+
result[entity_name] = MicroDataFrame(df, weights="household_weight")
|
|
156
|
+
else:
|
|
157
|
+
weight_col = f"{entity_name}_weight"
|
|
158
|
+
if weight_col in df.columns:
|
|
159
|
+
# Map new household weights to sub-entities via their
|
|
160
|
+
# household membership. Build a mapping from household_id
|
|
161
|
+
# to new weight.
|
|
162
|
+
hh_ids = household_df["household_id"].values
|
|
163
|
+
weight_map = dict(zip(hh_ids, region_weights))
|
|
164
|
+
|
|
165
|
+
# Find the entity's household ID column
|
|
166
|
+
person_hh_col = self._find_household_id_column(df, entity_name)
|
|
167
|
+
if person_hh_col:
|
|
168
|
+
new_weights = np.array(
|
|
169
|
+
[
|
|
170
|
+
weight_map.get(hh_id, 0.0)
|
|
171
|
+
for hh_id in df[person_hh_col].values
|
|
172
|
+
]
|
|
173
|
+
)
|
|
174
|
+
df[weight_col] = new_weights
|
|
175
|
+
|
|
176
|
+
result[entity_name] = MicroDataFrame(
|
|
177
|
+
df,
|
|
178
|
+
weights=(
|
|
179
|
+
f"{entity_name}_weight"
|
|
180
|
+
if f"{entity_name}_weight" in df.columns
|
|
181
|
+
else None
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _find_region_index(lookup_df: pd.DataFrame, region_code: str) -> int:
|
|
189
|
+
"""Find the row index for a region in the lookup CSV.
|
|
190
|
+
|
|
191
|
+
Searches by 'code' column first, then 'name' column.
|
|
192
|
+
"""
|
|
193
|
+
if "code" in lookup_df.columns and region_code in lookup_df["code"].values:
|
|
194
|
+
return lookup_df[lookup_df["code"] == region_code].index[0]
|
|
195
|
+
if "name" in lookup_df.columns and region_code in lookup_df["name"].values:
|
|
196
|
+
return lookup_df[lookup_df["name"] == region_code].index[0]
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"Region '{region_code}' not found in lookup CSV. "
|
|
199
|
+
f"Available columns: {list(lookup_df.columns)}. "
|
|
200
|
+
f"Searched 'code' and 'name' columns."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _find_household_id_column(df: pd.DataFrame, entity_name: str) -> str | None:
|
|
205
|
+
"""Find the column linking an entity to its household."""
|
|
206
|
+
candidates = [
|
|
207
|
+
"person_household_id",
|
|
208
|
+
f"{entity_name}_household_id",
|
|
209
|
+
"household_id",
|
|
210
|
+
]
|
|
211
|
+
for col in candidates:
|
|
212
|
+
if col in df.columns:
|
|
213
|
+
return col
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def cache_key(self) -> str:
|
|
218
|
+
return f"weight_replacement:{self.weight_matrix_key}:{self.region_code}"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
ScopingStrategy = Annotated[
|
|
222
|
+
RowFilterStrategy | WeightReplacementStrategy,
|
|
223
|
+
Discriminator("strategy_type"),
|
|
224
|
+
]
|
|
@@ -2,12 +2,13 @@ import logging
|
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from uuid import uuid4
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
6
|
|
|
7
7
|
from .cache import LRUCache
|
|
8
8
|
from .dataset import Dataset
|
|
9
9
|
from .dynamic import Dynamic
|
|
10
10
|
from .policy import Policy
|
|
11
|
+
from .scoping_strategy import RowFilterStrategy, ScopingStrategy
|
|
11
12
|
from .tax_benefit_model_version import TaxBenefitModelVersion
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
@@ -24,7 +25,13 @@ class Simulation(BaseModel):
|
|
|
24
25
|
dynamic: Dynamic | None = None
|
|
25
26
|
dataset: Dataset = None
|
|
26
27
|
|
|
27
|
-
#
|
|
28
|
+
# Scoping strategy (preferred over legacy filter fields)
|
|
29
|
+
scoping_strategy: ScopingStrategy | None = Field(
|
|
30
|
+
default=None,
|
|
31
|
+
description="Strategy for scoping dataset to a sub-national region",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Legacy regional filtering parameters (kept for backward compatibility)
|
|
28
35
|
filter_field: str | None = Field(
|
|
29
36
|
default=None,
|
|
30
37
|
description="Household-level variable to filter dataset by (e.g., 'place_fips', 'country')",
|
|
@@ -35,6 +42,25 @@ class Simulation(BaseModel):
|
|
|
35
42
|
)
|
|
36
43
|
|
|
37
44
|
tax_benefit_model_version: TaxBenefitModelVersion = None
|
|
45
|
+
|
|
46
|
+
@model_validator(mode="after")
|
|
47
|
+
def _auto_construct_strategy(self) -> "Simulation":
|
|
48
|
+
"""Auto-construct a RowFilterStrategy from legacy filter fields.
|
|
49
|
+
|
|
50
|
+
If filter_field and filter_value are set but scoping_strategy is not,
|
|
51
|
+
create a RowFilterStrategy for backward compatibility.
|
|
52
|
+
"""
|
|
53
|
+
if (
|
|
54
|
+
self.scoping_strategy is None
|
|
55
|
+
and self.filter_field is not None
|
|
56
|
+
and self.filter_value is not None
|
|
57
|
+
):
|
|
58
|
+
self.scoping_strategy = RowFilterStrategy(
|
|
59
|
+
variable_name=self.filter_field,
|
|
60
|
+
variable_value=self.filter_value,
|
|
61
|
+
)
|
|
62
|
+
return self
|
|
63
|
+
|
|
38
64
|
output_dataset: Dataset | None = None
|
|
39
65
|
|
|
40
66
|
def run(self):
|
{policyengine-3.2.1 → policyengine-3.2.4}/src/policyengine/core/tax_benefit_model_version.py
RENAMED
|
@@ -8,6 +8,7 @@ from .tax_benefit_model import TaxBenefitModel
|
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from .parameter import Parameter
|
|
11
|
+
from .parameter_node import ParameterNode
|
|
11
12
|
from .parameter_value import ParameterValue
|
|
12
13
|
from .region import Region, RegionRegistry
|
|
13
14
|
from .simulation import Simulation
|
|
@@ -25,6 +26,7 @@ class TaxBenefitModelVersion(BaseModel):
|
|
|
25
26
|
|
|
26
27
|
variables: list["Variable"] = Field(default_factory=list)
|
|
27
28
|
parameters: list["Parameter"] = Field(default_factory=list)
|
|
29
|
+
parameter_nodes: list["ParameterNode"] = Field(default_factory=list)
|
|
28
30
|
|
|
29
31
|
# Region registry for geographic simulations
|
|
30
32
|
region_registry: "RegionRegistry | None" = Field(
|
|
@@ -43,6 +45,9 @@ class TaxBenefitModelVersion(BaseModel):
|
|
|
43
45
|
parameters_by_name: dict[str, "Parameter"] = Field(
|
|
44
46
|
default_factory=dict, exclude=True
|
|
45
47
|
)
|
|
48
|
+
parameter_nodes_by_name: dict[str, "ParameterNode"] = Field(
|
|
49
|
+
default_factory=dict, exclude=True
|
|
50
|
+
)
|
|
46
51
|
|
|
47
52
|
def run(self, simulation: "Simulation") -> "Simulation":
|
|
48
53
|
raise NotImplementedError(
|
|
@@ -69,6 +74,11 @@ class TaxBenefitModelVersion(BaseModel):
|
|
|
69
74
|
self.variables.append(var)
|
|
70
75
|
self.variables_by_name[var.name] = var
|
|
71
76
|
|
|
77
|
+
def add_parameter_node(self, node: "ParameterNode") -> None:
|
|
78
|
+
"""Add a parameter node and index it for fast lookup."""
|
|
79
|
+
self.parameter_nodes.append(node)
|
|
80
|
+
self.parameter_nodes_by_name[node.name] = node
|
|
81
|
+
|
|
72
82
|
def get_parameter(self, name: str) -> "Parameter":
|
|
73
83
|
"""Get a parameter by name (O(1) lookup)."""
|
|
74
84
|
if name in self.parameters_by_name:
|
|
@@ -85,6 +95,14 @@ class TaxBenefitModelVersion(BaseModel):
|
|
|
85
95
|
f"Variable '{name}' not found in {self.model.id} version {self.version}"
|
|
86
96
|
)
|
|
87
97
|
|
|
98
|
+
def get_parameter_node(self, name: str) -> "ParameterNode":
|
|
99
|
+
"""Get a parameter node by name (O(1) lookup)."""
|
|
100
|
+
if name in self.parameter_nodes_by_name:
|
|
101
|
+
return self.parameter_nodes_by_name[name]
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"ParameterNode '{name}' not found in {self.model.id} version {self.version}"
|
|
104
|
+
)
|
|
105
|
+
|
|
88
106
|
def get_region(self, code: str) -> "Region | None":
|
|
89
107
|
"""Get a region by its code.
|
|
90
108
|
|
|
@@ -99,5 +117,5 @@ class TaxBenefitModelVersion(BaseModel):
|
|
|
99
117
|
return self.region_registry.get(code)
|
|
100
118
|
|
|
101
119
|
def __repr__(self) -> str:
|
|
102
|
-
# Give the id and version, and the number of variables, parameters, parameter values
|
|
103
|
-
return f"<TaxBenefitModelVersion id={self.id} variables={len(self.variables)} parameters={len(self.parameters)} parameter_values={len(self.parameter_values)}>"
|
|
120
|
+
# Give the id and version, and the number of variables, parameters, parameter nodes, parameter values
|
|
121
|
+
return f"<TaxBenefitModelVersion id={self.id} variables={len(self.variables)} parameters={len(self.parameters)} parameter_nodes={len(self.parameter_nodes)} parameter_values={len(self.parameter_values)}>"
|
|
@@ -8,6 +8,7 @@ from .tax_benefit_model_version import TaxBenefitModelVersion
|
|
|
8
8
|
class Variable(BaseModel):
|
|
9
9
|
id: str
|
|
10
10
|
name: str
|
|
11
|
+
label: str | None = None
|
|
11
12
|
tax_benefit_model_version: TaxBenefitModelVersion
|
|
12
13
|
entity: str
|
|
13
14
|
description: str | None = None
|
|
@@ -15,3 +16,5 @@ class Variable(BaseModel):
|
|
|
15
16
|
possible_values: list[Any] | None = None
|
|
16
17
|
default_value: Any = None
|
|
17
18
|
value_type: type | None = None
|
|
19
|
+
adds: list[str] | None = None
|
|
20
|
+
subtracts: list[str] | None = None
|