From 32e0ac04913fcc6e2c64fbe71071a6e3591a17ba Mon Sep 17 00:00:00 2001 From: Jason DeBacker Date: Tue, 1 Sep 2026 20:38:11 -0400 Subject: [PATCH 1/6] add git commands to makefile --- Makefile | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2c4c8fcbb..23e2d9cd6 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ # GNU Makefile that documents and automates common development operations # using the GNU make tool (version >= 3.81) -# Development is typically conducted on Linux or Max OS X (with the Xcode -# command-line tools installed), so this Makefile is designed -# to work in that environment (and not on Windows). +# Development is typically conducted on Linux or Mac OS X (with the Xcode +# command-line tools installed). The Git helper targets also +# work on Windows when GNU Make and uv are available. # USAGE: OG-Core$ make [TARGET] .PHONY: help @@ -17,6 +17,8 @@ help: @echo "coverage : generate test coverage report" @echo "documentation : build new Jupyter Book documentation files" @echo "format : format code using ruff and linecheck" + @echo "git-sync : synchronize local, origin, and upstream Git repos" + @echo "git-pr N=n : create local pr-n branch containing upstream PR" .PHONY: clean clean: @@ -72,3 +74,11 @@ format: .PHONY: pip-package pip-package: uv build + +.PHONY: git-sync +git-sync: + @uv run python ./gitsync + +.PHONY: git-pr +git-pr: + @uv run python ./gitpr $(N) From 911a9f162a27a55cc2e5cf86662ec195bf4a6e74 Mon Sep 17 00:00:00 2001 From: Jason DeBacker Date: Tue, 1 Sep 2026 20:38:44 -0400 Subject: [PATCH 2/6] add scripts for pr and sync --- gitpr | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ gitsync | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 gitpr create mode 100644 gitsync diff --git a/gitpr b/gitpr new file mode 100644 index 000000000..a4e6462ab --- /dev/null +++ b/gitpr @@ -0,0 +1,58 @@ +"""Create or update a local branch for an OG-Core upstream pull request.""" + +from __future__ import annotations + +import subprocess +import sys + + +def git(*args: str, check: bool = True, capture_output: bool = False) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ("git", *args), + check=check, + text=True, + capture_output=capture_output, + ) + + +def current_branch() -> str: + return git("branch", "--show-current", capture_output=True).stdout.strip() + + +def main() -> int: + if len(sys.argv) != 2 or not sys.argv[1].isdigit() or int(sys.argv[1]) < 1: + print("ERROR: specify one positive pull-request number: make git-pr N=123", file=sys.stderr) + return 1 + + branch = current_branch() + if branch not in {"master", "main"}: + print("STOP: switch to your local master or main branch first.", file=sys.stderr) + return 1 + + number = sys.argv[1] + pr_branch = f"pr-{number}" + git("fetch", "upstream", f"refs/pull/{number}/head") + + exists = git("show-ref", "--verify", "--quiet", f"refs/heads/{pr_branch}", check=False).returncode == 0 + if exists: + git("switch", pr_branch) + result = git("merge", "--ff-only", "FETCH_HEAD", check=False) + if result.returncode: + print( + f"STOP: {pr_branch} cannot fast-forward to upstream PR #{number}. " + "Delete or reconcile the local branch, then try again.", + file=sys.stderr, + ) + return result.returncode + else: + git("switch", "--create", pr_branch, "FETCH_HEAD") + + git("status", "--short", "--branch") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except subprocess.CalledProcessError as error: + raise SystemExit(error.returncode) diff --git a/gitsync b/gitsync new file mode 100644 index 000000000..96dfb95f3 --- /dev/null +++ b/gitsync @@ -0,0 +1,39 @@ +"""Fast-forward a local master or main branch from upstream/master.""" + +from __future__ import annotations + +import subprocess +import sys + + +def git(*args: str, capture_output: bool = False) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ("git", *args), + check=True, + text=True, + capture_output=capture_output, + ) + + +def current_branch() -> str: + return git("branch", "--show-current", capture_output=True).stdout.strip() + + +def main() -> int: + branch = current_branch() + if branch not in {"master", "main"}: + print("STOP: switch to your local master or main branch first.", file=sys.stderr) + return 1 + + git("fetch", "upstream") + # A sync should not silently create a merge commit when branches diverge. + git("merge", "--ff-only", "upstream/master") + git("push", "origin", branch) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except subprocess.CalledProcessError as error: + raise SystemExit(error.returncode) From fbc587552d0ea241dc4483bb0bd992ff5c8b2462 Mon Sep 17 00:00:00 2001 From: Jason DeBacker Date: Tue, 1 Sep 2026 22:01:12 -0400 Subject: [PATCH 3/6] wrap lines in changelog --- CHANGELOG.md | 68 +++++++++++----------------------------------------- 1 file changed, 14 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f3deb78..3cbfcff59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,70 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Fixes Issue [#1202](https://github.com/PSLmodels/OG-Core/issues/1202): - `get_r_gov` clipped the interest rate on government debt at zero, so a - sovereign that genuinely pays a negative real rate could not be modelled. - The bound is now a parameter, `r_gov_floor`, with a default of 0.0 that - reproduces the previous behaviour exactly. +- Fixes Issue [#1202](https://github.com/PSLmodels/OG-Core/issues/1202): `get_r_gov` clipped the interest rate on government debt at zero, so a sovereign that genuinely pays a negative real rate could not be modelled. The bound is now a parameter, `r_gov_floor`, with a default of 0.0 that reproduces the previous behaviour exactly. +- `npv_table` in `output_tables.py` (Issue #1131): builds a table of the net present value of the reform-minus-baseline change in flow variables (e.g. `Y`) over a horizon, evaluated at a list of discount rates. Values are un-stationarized by default so the NPV is taken over the actual (trend-inclusive) level path. +- New resource constraint error message, noting the maximum absolute resource-constraint error, the period it occurs in, and the tolerance, plus a one-line note on how to read it +- Scatter parameters object once per SS solve instead of one per residual evaluation. Pure overhead removal on any Dask-backed SS solve; the larger the Specifications object and the more workers, the more it saves. No numerical change — same solves, same results. + ### Bug Fixes -- Fixes Issue [#1200](https://github.com/PSLmodels/OG-Core/issues/1200): - `replacement_rate_adjust` was read only inside `SS_amount`, so it applied - to the US-Style Social Security system and was silently ignored under - Defined Benefits, Notional Defined Contribution, and Points System. The - adjustment is now applied to those three systems in `pension_amount`, - via a `replacement_rate_adjustment` helper that mirrors the indexing - `SS_amount` already uses, including the per-cohort `t + tt` offset along - the time path. `SS_amount` is unchanged, so US-Style results cannot move. - -### Added -- `npv_table` in `output_tables.py` (Issue #1131): builds a table of the net present value of the reform-minus-baseline change in flow variables (e.g. `Y`) over a horizon, evaluated at a list of discount rates. Values are un-stationarized by default so the NPV is taken over the actual (trend-inclusive) level path. +- Fixes Issue [#1200](https://github.com/PSLmodels/OG-Core/issues/1200): `replacement_rate_adjust` was read only inside `SS_amount`, so it applied to the US-Style Social Security system and was silently ignored under Defined Benefits, Notional Defined Contribution, and Points System. The adjustment is now applied to those three systems in `pension_amount`, via a `replacement_rate_adjustment` helper that mirrors the indexing `SS_amount` already uses, including the per-cohort `t + tt` offset along the time path. `SS_amount` is unchanged, so US-Style results cannot move. ## [0.20.0] - 2026-08-13 12:00:00 ### Added -- Addresses Issue [#1205](https://github.com/PSLmodels/OG-Core/issues/1205): - the UN Data Portal API token can now come from a `un_token` argument to - `demographics.get_un_data`, from a `UN_API_TOKEN` environment variable, or - from a single per-user file (`$XDG_CONFIG_HOME/og/un_api_token.txt`, or - `%APPDATA%\og\un_api_token.txt` on Windows). Sources are tried in that - order, then `un_api_token.txt` in the working directory. This gives one - token per user instead of one per directory. -- An `og-token` command, OG-Core's first console script, to manage that - token without hunting for the file: `og-token set` saves one, - `og-token show` reports where it lives and which source wins, and - `og-token rm` deletes it. The token is read without echoing and is never - printed back. +- Addresses Issue [#1205](https://github.com/PSLmodels/OG-Core/issues/1205): the UN Data Portal API token can now come from a `un_token` argument to `demographics.get_un_data`, from a `UN_API_TOKEN` environment variable, or from a single per-user file (`$XDG_CONFIG_HOME/og/un_api_token.txt`, or `%APPDATA%\og\un_api_token.txt` on Windows). Sources are tried in that order, then `un_api_token.txt` in the working directory. This gives one token per user instead of one per directory. +- An `og-token` command, OG-Core's first console script, to manage that token without hunting for the file: `og-token set` saves one, `og-token show` reports where it lives and which source wins, and `og-token rm` deletes it. The token is read without echoing and is never printed back. ### Changed -- Answering the UN API token prompt now saves the token to the per-user file - rather than to the current working directory, which used to leave a copy of - the token in every directory a model was run from. An existing - `un_api_token.txt` in the working directory is still read, with a notice - that the location is deprecated. -- The token prompt now names both ways forward: where to generate a free - token, and that pressing return uses the archived copy of the same data - instead. -- The token is read with `getpass` rather than `input`, so it is no longer - echoed into the terminal and its scrollback while being typed or pasted. -- An expired token is now named as the reason for falling back to the - archived data, instead of failing the same way as a missing one. The - portal issues JSON Web Tokens, so the expiry date is read locally with - no extra request, and `og-token show` reports whether the stored token - is still current. A token that is not a readable JSON Web Token is used - as before, with no expiry reported. -- A run that falls back to the archived data now says once, not once per - series, how to register a token: where to get one, the full path of the - `og-token` command belonging to the running interpreter, and the token - file to write. The full path matters because `og-token` is installed - beside the interpreter and is normally not on the shell's PATH, so - someone who obtains a token days later has something they can paste - rather than a command that reports "not found". -- The token prompt is skipped when standard input is not interactive, so - scheduled and scripted runs fall back to the Population-Data archive - instead of waiting on input. +- Answering the UN API token prompt now saves the token to the per-user file rather than to the current working directory, which used to leave a copy of the token in every directory a model was run from. An existing `un_api_token.txt` in the working directory is still read, with a notice that the location is deprecated. +- The token prompt now names both ways forward: where to generate a free token, and that pressing return uses the archived copy of the same data instead. +- The token is read with `getpass` rather than `input`, so it is no longer echoed into the terminal and its scrollback while being typed or pasted. +- An expired token is now named as the reason for falling back to the archived data, instead of failing the same way as a missing one. The portal issues JSON Web Tokens, so the expiry date is read locally with no extra request, and `og-token show` reports whether the stored token is still current. A token that is not a readable JSON Web Token is used as before, with no expiry reported. +- A run that falls back to the archived data now says once, not once per series, how to register a token: where to get one, the full path of the `og-token` command belonging to the running interpreter, and the token file to write. The full path matters because `og-token` is installed beside the interpreter and is normally not on the shell's PATH, so someone who obtains a token days later has something they can paste rather than a command that reports "not found". +- The token prompt is skipped when standard input is not interactive, so scheduled and scripted runs fall back to the Population-Data archive instead of waiting on input. ## [0.19.2] - 2026-08-18 17:00:00 From 8a8eb9d052dcfc6cb86f9bc061b43de42a9eb1ba Mon Sep 17 00:00:00 2001 From: Jason DeBacker Date: Wed, 2 Sep 2026 19:21:41 -0400 Subject: [PATCH 4/6] add py file extensions --- gitpr => gitpr.py | 0 gitsync => gitsync.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename gitpr => gitpr.py (100%) rename gitsync => gitsync.py (100%) diff --git a/gitpr b/gitpr.py similarity index 100% rename from gitpr rename to gitpr.py diff --git a/gitsync b/gitsync.py similarity index 100% rename from gitsync rename to gitsync.py From 68e51f27468a821596da1968fd83aefdc4f8395e Mon Sep 17 00:00:00 2001 From: Jason DeBacker Date: Wed, 2 Sep 2026 19:22:57 -0400 Subject: [PATCH 5/6] format --- gitpr.py | 25 +++++++++++++++++++++---- gitsync.py | 9 +++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/gitpr.py b/gitpr.py index a4e6462ab..09fa681c3 100644 --- a/gitpr.py +++ b/gitpr.py @@ -6,7 +6,9 @@ import sys -def git(*args: str, check: bool = True, capture_output: bool = False) -> subprocess.CompletedProcess[str]: +def git( + *args: str, check: bool = True, capture_output: bool = False +) -> subprocess.CompletedProcess[str]: return subprocess.run( ("git", *args), check=check, @@ -21,19 +23,34 @@ def current_branch() -> str: def main() -> int: if len(sys.argv) != 2 or not sys.argv[1].isdigit() or int(sys.argv[1]) < 1: - print("ERROR: specify one positive pull-request number: make git-pr N=123", file=sys.stderr) + print( + "ERROR: specify one positive pull-request number: make git-pr N=123", + file=sys.stderr, + ) return 1 branch = current_branch() if branch not in {"master", "main"}: - print("STOP: switch to your local master or main branch first.", file=sys.stderr) + print( + "STOP: switch to your local master or main branch first.", + file=sys.stderr, + ) return 1 number = sys.argv[1] pr_branch = f"pr-{number}" git("fetch", "upstream", f"refs/pull/{number}/head") - exists = git("show-ref", "--verify", "--quiet", f"refs/heads/{pr_branch}", check=False).returncode == 0 + exists = ( + git( + "show-ref", + "--verify", + "--quiet", + f"refs/heads/{pr_branch}", + check=False, + ).returncode + == 0 + ) if exists: git("switch", pr_branch) result = git("merge", "--ff-only", "FETCH_HEAD", check=False) diff --git a/gitsync.py b/gitsync.py index 96dfb95f3..b65998c25 100644 --- a/gitsync.py +++ b/gitsync.py @@ -6,7 +6,9 @@ import sys -def git(*args: str, capture_output: bool = False) -> subprocess.CompletedProcess[str]: +def git( + *args: str, capture_output: bool = False +) -> subprocess.CompletedProcess[str]: return subprocess.run( ("git", *args), check=True, @@ -22,7 +24,10 @@ def current_branch() -> str: def main() -> int: branch = current_branch() if branch not in {"master", "main"}: - print("STOP: switch to your local master or main branch first.", file=sys.stderr) + print( + "STOP: switch to your local master or main branch first.", + file=sys.stderr, + ) return 1 git("fetch", "upstream") From 1d678e79ecc8409688582e9de33c287c2ebeeb98 Mon Sep 17 00:00:00 2001 From: Jason DeBacker Date: Wed, 2 Sep 2026 19:34:50 -0400 Subject: [PATCH 6/6] format --- gitpr.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gitpr.py b/gitpr.py index 09fa681c3..e2a4e91fe 100644 --- a/gitpr.py +++ b/gitpr.py @@ -24,7 +24,8 @@ def current_branch() -> str: def main() -> int: if len(sys.argv) != 2 or not sys.argv[1].isdigit() or int(sys.argv[1]) < 1: print( - "ERROR: specify one positive pull-request number: make git-pr N=123", + "ERROR: specify one positive pull-request number: make " + "git-pr N=123", file=sys.stderr, ) return 1 @@ -56,7 +57,8 @@ def main() -> int: result = git("merge", "--ff-only", "FETCH_HEAD", check=False) if result.returncode: print( - f"STOP: {pr_branch} cannot fast-forward to upstream PR #{number}. " + "STOP: {pr_branch} cannot fast-forward to upstream " + f"PR #{number}. " "Delete or reconcile the local branch, then try again.", file=sys.stderr, )