From 5f9e069409526e9125692c0da7b0a5fb98362898 Mon Sep 17 00:00:00 2001 From: "numerai-hosted-agent-bot[bot]" <247676148+numerai-hosted-agent-bot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:17:47 -0700 Subject: [PATCH 01/11] NumerAPI 3.0: payout multiplier removal (#134) --- CHANGELOG.md | 4 ++ docs/round-score-configs.md | 33 ++++--------- numerapi/base_api.py | 56 --------------------- setup.py | 2 +- tests/test_base_api.py | 97 +++++++------------------------------ 5 files changed, 33 insertions(+), 159 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e643e0d..e640445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog Notable changes to this project. +## [3.0.0] - Unreleased +- remove the six deprecated Corr/MMC multiplier projections from + `list_rounds`; use the identity-preserving `roundScoreConfigs` list instead + ## [2.24.0] - 2026-08-03 - add exact `roundScoreConfigs` identities, scoring windows, and payout settings to `list_rounds` for Classic, Signals, and Crypto diff --git a/docs/round-score-configs.md b/docs/round-score-configs.md index 99db25d..bac0613 100644 --- a/docs/round-score-configs.md +++ b/docs/round-score-configs.md @@ -21,7 +21,7 @@ Each item includes: consistent with other date fields in numerapi. GraphQL float and integer fields retain their normal Python JSON types. -## Migrating from legacy multiplier keys +## Legacy multiplier keys removed in 3.0.0 Before 2.24.0, `list_rounds()` requested server compatibility fields. For a Signals round, a response could look like this even though the payout scores @@ -34,8 +34,8 @@ were Alpha and MPC: } ``` -In 2.24.0 the exact identities are available without knowing score names in -advance: +In 3.0.0, the exact identities are the only payout configuration returned by +`list_rounds()`: ```python { @@ -57,29 +57,16 @@ advance: "isPayout": True, "defaultMultiplier": 0.8, }, - ], - "defaultCorrMultiplier": None, - "defaultMmcMultiplier": None, + ] } ``` -The six established Corr/MMC keys (`min`, `max`, and `default` for each) stay -in the returned round dictionary throughout numerapi 2.x. They are now -identity-safe projections: Corr keys select only a payout config whose `name` -is exactly `correlation`, MMC keys select only a payout config whose `name` is -exactly `meta_model_contribution`, and the keys are `None` when there is no -exact match. Alpha and FNC are never projected as Corr; MPC is never projected -as MMC. If multiple exact payout configs exist, the projection uses the config -with the newest `roundNumberStart`, then compares the numeric `version` values -as integers and uses `id` for a numeric-version tie. If multiple configs at the -newest start contain a non-numeric future version, the compatibility keys are -`None` rather than guessing an order. The complete list remains available -unchanged in either case. - -These six compatibility keys are scheduled for removal in numerapi 3.0.0. -`list_rounds()` never exposed the three legacy TC multiplier fields, so this -migration does not introduce them. Code should migrate now by filtering -`roundScoreConfigs`, normally starting with `isPayout`. +The six Corr/MMC compatibility keys (`min`, `max`, and `default` for each) are +no longer added to the returned round dictionary. `list_rounds()` never +exposed the three legacy TC multiplier fields. Code should filter +`roundScoreConfigs`, normally starting with `isPayout`, and preserve each +configuration's `name`, `version`, and `scoreConfigId` rather than projecting +different scores into Corr or MMC roles. ## Deprecated performance endpoint diff --git a/numerapi/base_api.py b/numerapi/base_api.py index 8d332c4..afde839 100644 --- a/numerapi/base_api.py +++ b/numerapi/base_api.py @@ -751,13 +751,6 @@ def list_rounds( list of dicts: round entries matching the provided filters. Each entry includes ``roundScoreConfigs``, whose items retain the exact score identity and per-round payout settings returned by the API. - - The legacy ``minCorrMultiplier`` through - ``defaultMmcMultiplier`` keys remain until numerapi 3.0.0. They are - compatibility projections of payout configs whose names are - exactly ``correlation`` or ``meta_model_contribution``; they are - ``None`` when no such payout config exists. Use - ``roundScoreConfigs`` for all new integrations. """ query = """ query($tournament: Int @@ -835,57 +828,8 @@ def list_rounds( utils.replace( config, "scoringEnd", utils.parse_datetime_string ) - self._add_legacy_round_multipliers(round_info) return rounds - @staticmethod - def _add_legacy_round_multipliers(round_info: dict) -> None: - """Add deprecated, identity-safe round multiplier projections.""" - legacy_scores = { - "Corr": "correlation", - "Mmc": "meta_model_contribution", - } - multiplier_fields = { - "min": "minMultiplier", - "max": "maxMultiplier", - "default": "defaultMultiplier", - } - - for legacy_name, score_name in legacy_scores.items(): - matches = [ - config - for config in round_info["roundScoreConfigs"] - if config["isPayout"] and config["name"] == score_name - ] - config = Api._select_legacy_round_config(matches) - for prefix, config_field in multiplier_fields.items(): - field = f"{prefix}{legacy_name}Multiplier" - round_info[field] = ( - None if config is None else config[config_field] - ) - - @staticmethod - def _select_legacy_round_config(configs: List[Dict]) -> Dict | None: - """Select the latest config, failing closed on ambiguous versions.""" - if not configs: - return None - - latest_start = max(item["roundNumberStart"] for item in configs) - candidates = [ - item for item in configs if item["roundNumberStart"] == latest_start - ] - if len(candidates) == 1: - return candidates[0] - - try: - return max( - candidates, - key=lambda item: (int(item["version"]), item["id"]), - ) - except (TypeError, ValueError): - # A future non-numeric version contract cannot be ordered safely. - return None - def set_bio(self, model_id: str, bio: str) -> bool: """Set bio field for a model id. diff --git a/setup.py b/setup.py index 1d93053..2e25bd8 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ def load(path): return open(path, "r").read() -numerapi_version = "2.24.0" +numerapi_version = "3.0.0" classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/tests/test_base_api.py b/tests/test_base_api.py index 7e832f1..d10622a 100644 --- a/tests/test_base_api.py +++ b/tests/test_base_api.py @@ -369,8 +369,14 @@ def test_list_rounds(api): res[0]["roundScoreConfigs"][0]["scoringEnd"], datetime.datetime ) assert res[0]["roundScoreConfigs"][0]["scoreConfigId"] == "classic-corr" - assert res[0]["defaultCorrMultiplier"] == 0.75 - assert res[0]["defaultMmcMultiplier"] == 2.25 + assert not { + "minCorrMultiplier", + "maxCorrMultiplier", + "defaultCorrMultiplier", + "minMmcMultiplier", + "maxMmcMultiplier", + "defaultMmcMultiplier", + }.intersection(res[0]) request_body = json.loads(responses.calls[0].request.body) assert request_body["variables"]["tournament"] == 8 @@ -403,18 +409,23 @@ def test_list_rounds(api): } assert "roundScoreConfigs" in request_body["query"] assert all(field in request_body["query"] for field in requested_fields) - assert "minCorrMultiplier" not in request_body["query"] - assert "minMmcMultiplier" not in request_body["query"] + assert not { + "minCorrMultiplier", + "maxCorrMultiplier", + "defaultCorrMultiplier", + "minMmcMultiplier", + "maxMmcMultiplier", + "defaultMmcMultiplier", + }.intersection(request_body["query"].split()) @pytest.mark.parametrize( - ("api_class", "tournament", "score_names", "legacy_multipliers"), + ("api_class", "tournament", "score_names"), [ ( numerapi.NumerAPI, 8, ["correlation", "meta_model_contribution"], - (0.5, 0.5), ), ( numerapi.SignalsAPI, @@ -424,19 +435,17 @@ def test_list_rounds(api): "v4_feature_neutral_correlation", "meta_portfolio_contribution", ], - (None, None), ), ( numerapi.CryptoAPI, 12, ["correlation", "meta_model_contribution"], - (0.5, 0.5), ), ], ) @responses.activate def test_list_rounds_preserves_tournament_score_identities( - api_class, tournament, score_names, legacy_multipliers + api_class, tournament, score_names ): api = api_class() configs = [_round_score_config(name) for name in score_names] @@ -452,8 +461,6 @@ def test_list_rounds_preserves_tournament_score_identities( assert [ config["name"] for config in returned_round["roundScoreConfigs"] ] == score_names - assert returned_round["defaultCorrMultiplier"] == legacy_multipliers[0] - assert returned_round["defaultMmcMultiplier"] == legacy_multipliers[1] request_body = json.loads(responses.calls[0].request.body) assert request_body["variables"]["tournament"] == tournament @@ -492,74 +499,6 @@ def test_list_rounds_keeps_coexisting_and_unfamiliar_score_configs(api): config["scoreConfigId"] for config in returned_round["roundScoreConfigs"] ] == [config["scoreConfigId"] for config in configs] - assert returned_round["defaultCorrMultiplier"] == 0.4 - assert returned_round["defaultMmcMultiplier"] == 0.6 - - -@responses.activate -def test_list_rounds_orders_numeric_score_versions_numerically(api): - configs = [ - _round_score_config( - "correlation", - config_id="corr-9", - version="9", - multiplier=0.9, - round_number_start=200, - ), - _round_score_config( - "correlation", - config_id="corr-10", - version="10", - multiplier=1.0, - round_number_start=200, - ), - ] - responses.add( - responses.POST, - base_api.API_TOURNAMENT_URL, - json={"data": {"rounds": [{"roundScoreConfigs": configs}]}}, - ) - - returned_round = api.list_rounds()[0] - - assert [ - returned_round["minCorrMultiplier"], - returned_round["maxCorrMultiplier"], - returned_round["defaultCorrMultiplier"], - ] == [1.0, 1.0, 1.0] - - -@responses.activate -def test_list_rounds_fails_closed_for_ambiguous_non_numeric_versions(api): - configs = [ - _round_score_config( - "correlation", - config_id="corr-10", - version="10", - multiplier=1.0, - round_number_start=200, - ), - _round_score_config( - "correlation", - config_id="corr-next", - version="next", - multiplier=1.1, - round_number_start=200, - ), - ] - responses.add( - responses.POST, - base_api.API_TOURNAMENT_URL, - json={"data": {"rounds": [{"roundScoreConfigs": configs}]}}, - ) - - returned_round = api.list_rounds()[0] - - assert [ - returned_round["minCorrMultiplier"], - returned_round["maxCorrMultiplier"], - returned_round["defaultCorrMultiplier"], - ] == [None, None, None] @responses.activate From a2af069df929fb5198c1f29e064c9adb786c0a33 Mon Sep 17 00:00:00 2001 From: Josh Terrill Date: Mon, 10 Aug 2026 13:24:18 -0700 Subject: [PATCH 02/11] adding support for python 3.14 and pandas 3 --- .github/workflows/pytest.yml | 20 +++++++++++++------- .gitignore | 1 + requirements.txt | 3 ++- requirements_tests.txt | 3 ++- setup.py | 15 +++++++++++---- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c44f916..fa7e210 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -5,22 +5,28 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.10 - uses: actions/setup-python@v2 + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements_tests.txt + python -m pip install . -r requirements_tests.txt + - name: Require pandas 3 on Python 3.14 + if: matrix.python-version == '3.14' + run: python -m pip install "pandas>=3,<4" - name: Run tests run: python -m pytest --import-mode=append tests/ --cov=./ --cov-report=xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v5 with: directory: ./coverage/reports/ fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index 6f4eba1..3aa711a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/requirements.txt b/requirements.txt index 156d614..403675b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ pytz tqdm>=4.29.1 click>=7.0 fsspec[http] -pandas>=1.1.0 +pandas>=1.1.0; python_version < "3.14" +pandas>=2.3.3; python_version >= "3.14" diff --git a/requirements_tests.txt b/requirements_tests.txt index 2916994..8b9980f 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -3,4 +3,5 @@ pytest-cov codecov responses flake8 -pandas>=1.1.0 +pandas>=1.1.0; python_version < "3.14" +pandas>=2.3.3; python_version >= "3.14" diff --git a/setup.py b/setup.py index 2e25bd8..b3c73cd 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,11 @@ def load(path): "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", ] @@ -23,14 +28,15 @@ def load(path): setup( name="numerapi", version=numerapi_version, - maintainer="uuazed", - maintainer_email="uuazed@gmail.com", + maintainer="Numerai", + maintainer_email="support@gmail.com", description="Automatically download and upload data for the Numerai machine learning competition", long_description=load("README.md"), long_description_content_type="text/markdown", - url="https://github.com/uuazed/numerapi", + url="https://github.com/numerai/numerapi", platforms="OS Independent", classifiers=classifiers, + python_requires=">=3.10", license="MIT License", package_data={"numerapi": ["LICENSE", "README.md", "py.typed"]}, packages=find_packages(exclude=["tests"]), @@ -41,7 +47,8 @@ def load(path): "tqdm>=4.29.1", "click>=7.0", "fsspec[http]", - "pandas>=1.1.0", + "pandas>=1.1.0; python_version < '3.14'", + "pandas>=2.3.3; python_version >= '3.14'", ], entry_points={"console_scripts": ["numerapi = numerapi.cli:cli"]}, ) From 35cd6f9b86b2f286896d6177f72f2ba9ab31de5c Mon Sep 17 00:00:00 2001 From: Josh Terrill Date: Mon, 10 Aug 2026 13:33:01 -0700 Subject: [PATCH 03/11] incremented version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b3c73cd..73f94a5 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ def load(path): return open(path, "r").read() -numerapi_version = "3.0.0" +numerapi_version = "3.1.0" classifiers = [ "Development Status :: 5 - Production/Stable", From 5da1ffd49856bd7756ff7a8ee0c4f892a3663b0d Mon Sep 17 00:00:00 2001 From: Josh Terrill Date: Mon, 10 Aug 2026 14:38:01 -0700 Subject: [PATCH 04/11] numerapi 3.1.2.dev0 - use canonical PEP 440 spelling for the version (3.1.2.dev0) - changelog: open 3.1.2, date the published 3.0.0 entry, note that 3.1.0/3.1.1 were never released - pypi workflow: only publish from v* tags so the `stable` docs pointer can no longer trigger a release, require the tag to match setup.py's version, and require final (non-prerelease) tags to point at a commit on master - pypi workflow: build with `python -m build` instead of the deprecated `setup.py sdist bdist_wheel` Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/pypi.yml | 57 ++++++++++++++++++++++++++++++++++++-- CHANGELOG.md | 9 +++++- setup.py | 2 +- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 070cc06..a9c19c8 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -3,7 +3,9 @@ name: Upload Python Package on: push: tags: - - "*" + # Version tags only. Non-version tags such as `stable` (the Read the Docs + # pointer) must never trigger a publish. + - "v[0-9]*" jobs: build-n-publish: @@ -11,6 +13,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + # full history so the master-ancestry check below can run + fetch-depth: 0 - name: Set up Python 3.10 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 with: @@ -19,11 +24,57 @@ jobs: run: >- python -m pip install - build wheel + build wheel packaging --user + - name: Verify tag matches setup.py version + env: + TAG_NAME: ${{ github.ref_name }} + run: | + python - <<'PY' + import os + import pathlib + import re + import sys + from packaging.version import Version + + tag = os.environ["TAG_NAME"].removeprefix("v") + pkg = re.search( + r'numerapi_version\s*=\s*"([^"]+)"', + pathlib.Path("setup.py").read_text(), + ).group(1) + print(f"tag={tag} setup.py={pkg}") + if Version(tag) != Version(pkg): + sys.exit( + f"::error::Tag {os.environ['TAG_NAME']} does not match " + f"setup.py numerapi_version={pkg}" + ) + print(f"::notice::publishing numerapi {Version(pkg)}") + PY + - name: Final releases must come from master + env: + TAG_NAME: ${{ github.ref_name }} + run: | + python - <<'PY' > prerelease.txt + import os + from packaging.version import Version + + print("yes" if Version(os.environ["TAG_NAME"].removeprefix("v")).is_prerelease else "no") + PY + if [ "$(cat prerelease.txt)" = "no" ]; then + git fetch --no-tags --quiet origin master + if git merge-base --is-ancestor "$GITHUB_SHA" FETCH_HEAD; then + echo "final release from a commit on master: ok" + else + echo "::error::Final release tags must point at a commit on master; $GITHUB_SHA is not one. Cut pre-releases (e.g. 3.1.2.dev0) from preview instead." + exit 1 + fi + else + echo "pre-release: master-ancestry check skipped" + fi + rm -f prerelease.txt - name: Build a binary wheel and a source tarball run: >- - python setup.py sdist && python setup.py bdist_wheel + python -m build - name: Publish distribution 📦 to PyPI if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b diff --git a/CHANGELOG.md b/CHANGELOG.md index e640445..9623222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,14 @@ # Changelog Notable changes to this project. -## [3.0.0] - Unreleased +## [3.1.2] - Unreleased +- support Python 3.14 and pandas 3: require `pandas>=2.3.3` on Python 3.14 and + keep `pandas>=1.1.0` below it +- declare `python_requires>=3.10` and advertise Python 3.10 through 3.14 +- 3.1.0 and 3.1.1 were never released; 3.1.0 was published briefly and + withdrawn from PyPI + +## [3.0.0] - 2026-08-07 - remove the six deprecated Corr/MMC multiplier projections from `list_rounds`; use the identity-preserving `roundScoreConfigs` list instead diff --git a/setup.py b/setup.py index 73f94a5..533e293 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ def load(path): return open(path, "r").read() -numerapi_version = "3.1.0" +numerapi_version = "3.1.2.dev0" classifiers = [ "Development Status :: 5 - Production/Stable", From 6390f672641bad0c70796ff66dbdb112731c6700 Mon Sep 17 00:00:00 2001 From: Josh Terrill Date: Mon, 10 Aug 2026 15:26:51 -0700 Subject: [PATCH 05/11] added releasing document and fixed maintainer email address --- RELEASING.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 RELEASING.md diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..be4215f --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,192 @@ +# Releasing numerapi + +numerapi ships to **PyPI only**. There are no container images, no ECR, and no +branch-triggered deploys in this repo — if you are thinking of the +`master` → prod / `staging` → staging image flow from our other repos, that does +not apply here. + +## The model + +Two rules explain everything else: + +1. **`numerapi_version` in `setup.py` is the release.** The git tag is only the + trigger. Whatever that string says is what lands on PyPI. +2. **The version string picks the channel**, not the branch. A + [PEP 440](https://peps.python.org/pep-0440/) pre-release (`3.2.0.dev0`) is + invisible to `pip install numerapi`; a final version (`3.2.0`) is what + everyone gets by default. + +| Ref | Role | +| --- | --- | +| `/` | all work; branch off `preview` | +| `preview` | integration branch; **beta** releases are cut here | +| `master` | released state; **final** releases are cut here | +| `vX.Y.Z.devN` tag | publishes a pre-release | +| `vX.Y.Z` tag | publishes a final release | + +Nothing publishes on a branch push. Only pushing a tag publishes. + +| What a user runs | What they get | +| --- | --- | +| `pip install numerapi` / `pip install -U numerapi` | latest **final** version | +| `pip install 'numerapi==3.2.0.dev0'` | that exact pre-release | +| `pip install --pre numerapi` | latest including pre-releases | + +## Conventions + +- Tags are `v`-prefixed and must match `setup.py` exactly: `v3.2.0`, + `v3.2.0.dev0`. CI rejects a mismatch. +- Use the canonical PEP 440 spelling with the dot: `3.2.0.dev0`, not `3.2.0dev0`. + Both normalize to the same release, but the canonical form avoids confusion. +- Pre-releases use `.devN`. Increment `N` for each beta on the same version line. +- **A version number can never be reused.** PyPI permanently rejects re-uploading + a version, even one that was deleted. If you burn a number, move to the next. + +## Develop without releasing + +```bash +git checkout preview && git pull +git checkout -b josh/some-feature +# ... work ... +git push -u origin josh/some-feature +gh pr create --base preview +``` + +Tests and lint run on every push. No tag means nothing is published. `preview` +can sit ahead of `master` indefinitely — that is what it is for. Leave +`setup.py` alone until you are actually cutting something. + +## Cut a beta (from `preview`) + +For beta users who need the code before it is stable. + +```bash +git checkout preview && git pull + +# 1. setup.py: numerapi_version = "3.2.0.dev0" +# 2. CHANGELOG.md: open an entry +# ## [3.2.0] - Unreleased +# - what changed +git commit -am "numerapi 3.2.0.dev0" +git push origin preview # publishes nothing + +# 3. tag and push — this is the release event +git tag v3.2.0.dev0 +git push origin v3.2.0.dev0 +``` + +Verify: + +```bash +gh run list --workflow=pypi.yml --limit 1 # expect success +pip install 'numerapi==3.2.0.dev0' # what beta users run +pip install -U numerapi # must NOT be the dev version +``` + +Tell beta users to install the exact version. Note that `pip index versions` and +the simple index can lag a few minutes behind a successful publish on CDN cache; +an exact-version install works immediately. + +For the next beta, repeat with `.dev1`, `.dev2`, … + +## Promote to a final release (from `master`) + +Flip the version to final **on `preview`, as the last commit before merging**, so +`master` never holds a pre-release string and picks up the release version +atomically at merge. + +```bash +git checkout preview && git pull + +# 1. setup.py: numerapi_version = "3.2.0" (drop the .devN suffix) +# 2. CHANGELOG.md: date the entry, e.g. ## [3.2.0] - 2026-08-17 +git commit -am "numerapi 3.2.0" +git push origin preview + +# 3. merge preview into master +gh pr create --base master --head preview --title "numerapi 3.2.0" +gh pr merge --merge + +# 4. tag master +git checkout master && git pull +grep numerapi_version setup.py # must read exactly 3.2.0 +git tag v3.2.0 +git push origin v3.2.0 + +# 5. keep preview caught up so it does not drift +git checkout preview && git merge master && git push origin preview +``` + +Verify with `pip install -U numerapi`. + +Then bump the numerapi pin in `tournament-monorepo` (`shared`, `init-round`, +`integration-test`, `compute-pickle-scheduler`). That PR moving through the +monorepo's own staging → master is what carries the new numerapi into staging +and prod images. Never pin a `.devN` version in anything that reaches prod. + +## Hotfix a released version + +Use this when `master` is released and `preview` holds unreleased work you do not +want to ship yet. + +```bash +git checkout -b hotfix/3.2.1 master # branch off master, NOT preview +# fix + setup.py 3.2.1 + CHANGELOG entry +gh pr create --base master +# after merge: +git checkout master && git pull +git tag v3.2.1 && git push origin v3.2.1 +git checkout preview && git merge master && git push origin preview +``` + +## Documentation + +Read the Docs is fully automatic — there is nothing to tag or move. + +- `/en/latest/` tracks `master`. +- `/en/stable/` tracks the greatest **non-pre-release** semver tag, so cutting + `v3.2.0` promotes it; `v3.2.0.dev0` is correctly ignored. + +Do not create a tag or branch named `stable`. That overrides the automatic +behavior above, has to be force-moved by hand on every release, and silently goes +stale when someone forgets. One used to exist here and was removed for exactly +those reasons. The trade-off is that a docs-only fix reaches `/en/latest/` +immediately but does not appear on `/en/stable/` until the next release; if that +matters, cut a patch release. + +## What CI enforces + +`.github/workflows/pypi.yml` runs on tag pushes matching `v[0-9]*` and will +refuse to publish unless: + +1. **The tag matches `setup.py`.** Compared as normalized PEP 440 versions, so + `v3.2.0dev0` and `v3.2.0.dev0` are equivalent, but `v3.2.0` against a + `setup.py` of `3.2.0.dev0` fails. +2. **Final releases point at a commit on `master`.** Pre-releases skip this + check, so betas can be cut from `preview` but a final one cannot. + +`pytest.yml` (Python 3.10–3.14) and `ruff.yml` run on every push and PR. + +## Troubleshooting + +**`File already exists` on publish.** That version is already on PyPI. Bump to +the next number — you cannot re-upload, and you cannot fix it by deleting the +release on PyPI either. + +**Tag mismatch error.** You tagged without bumping `setup.py`, or vice versa. Fix +`setup.py`, commit, delete the tag locally and on origin +(`git push origin :refs/tags/vX.Y.Z`), then re-tag. Deleting a tag never +publishes anything. + +**"Final release tags must point at a commit on master."** You tagged a +suffix-free version on `preview`. Either merge to `master` first, or cut it as a +`.devN` pre-release instead. + +**Do not retro-tag old releases.** Any new tag matching `v[0-9]*` triggers a +publish attempt that will fail on a duplicate version. Historical tags are +inconsistent (some `v`-prefixed, some not, several `.devN` tags that published +final versions before the guards existed) — leave them as they are. + +**Do not delete the `3.0.0.dev2` tag.** The commit it points at is on no branch, +and that tag is the only thing keeping the source of the published 3.0.0 +reachable. diff --git a/setup.py b/setup.py index 533e293..514fe2e 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ def load(path): name="numerapi", version=numerapi_version, maintainer="Numerai", - maintainer_email="support@gmail.com", + maintainer_email="tournament@numer.ai", description="Automatically download and upload data for the Numerai machine learning competition", long_description=load("README.md"), long_description_content_type="text/markdown", From b7aff094ceea0dd95ad8606b0bead2b50cc1beba Mon Sep 17 00:00:00 2001 From: Josh Terrill Date: Mon, 10 Aug 2026 15:32:53 -0700 Subject: [PATCH 06/11] numerapi 3.1.2.dev1 - release tags are bare version numbers; a `v` prefix is no longer valid and is now rejected with an explicit error rather than silently ignored - add RELEASING.md documenting the preview/master flow, the two release channels, hotfixes, and the failure modes CI guards against Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/pypi.yml | 20 ++++++++++++------- RELEASING.md | 41 ++++++++++++++++++++------------------ setup.py | 2 +- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index a9c19c8..41bf977 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -3,8 +3,10 @@ name: Upload Python Package on: push: tags: - # Version tags only. Non-version tags such as `stable` (the Read the Docs - # pointer) must never trigger a publish. + # Release tags are bare version numbers: 3.1.2, 3.1.2.dev0 + - "[0-9]*" + # A `v` prefix is NOT a valid release tag. It is matched here only so the + # job can fail with an explanatory error instead of silently doing nothing. - "v[0-9]*" jobs: @@ -26,7 +28,7 @@ jobs: pip install build wheel packaging --user - - name: Verify tag matches setup.py version + - name: Verify tag format and version match env: TAG_NAME: ${{ github.ref_name }} run: | @@ -37,7 +39,12 @@ jobs: import sys from packaging.version import Version - tag = os.environ["TAG_NAME"].removeprefix("v") + tag = os.environ["TAG_NAME"] + if tag.startswith("v"): + sys.exit( + f"::error::Release tags must be bare version numbers. " + f"`{tag}` has a `v` prefix; delete it and tag `{tag[1:]}` instead." + ) pkg = re.search( r'numerapi_version\s*=\s*"([^"]+)"', pathlib.Path("setup.py").read_text(), @@ -45,8 +52,7 @@ jobs: print(f"tag={tag} setup.py={pkg}") if Version(tag) != Version(pkg): sys.exit( - f"::error::Tag {os.environ['TAG_NAME']} does not match " - f"setup.py numerapi_version={pkg}" + f"::error::Tag {tag} does not match setup.py numerapi_version={pkg}" ) print(f"::notice::publishing numerapi {Version(pkg)}") PY @@ -58,7 +64,7 @@ jobs: import os from packaging.version import Version - print("yes" if Version(os.environ["TAG_NAME"].removeprefix("v")).is_prerelease else "no") + print("yes" if Version(os.environ["TAG_NAME"]).is_prerelease else "no") PY if [ "$(cat prerelease.txt)" = "no" ]; then git fetch --no-tags --quiet origin master diff --git a/RELEASING.md b/RELEASING.md index be4215f..3769649 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -21,8 +21,8 @@ Two rules explain everything else: | `/` | all work; branch off `preview` | | `preview` | integration branch; **beta** releases are cut here | | `master` | released state; **final** releases are cut here | -| `vX.Y.Z.devN` tag | publishes a pre-release | -| `vX.Y.Z` tag | publishes a final release | +| `X.Y.Z.devN` tag | publishes a pre-release | +| `X.Y.Z` tag | publishes a final release | Nothing publishes on a branch push. Only pushing a tag publishes. @@ -34,8 +34,9 @@ Nothing publishes on a branch push. Only pushing a tag publishes. ## Conventions -- Tags are `v`-prefixed and must match `setup.py` exactly: `v3.2.0`, - `v3.2.0.dev0`. CI rejects a mismatch. +- **Tags are bare version numbers and must match `setup.py` exactly:** `3.2.0`, + `3.2.0.dev0`. A `v` prefix is not allowed — CI rejects `v3.2.0` with an + explicit error. - Use the canonical PEP 440 spelling with the dot: `3.2.0.dev0`, not `3.2.0dev0`. Both normalize to the same release, but the canonical form avoids confusion. - Pre-releases use `.devN`. Increment `N` for each beta on the same version line. @@ -71,8 +72,8 @@ git commit -am "numerapi 3.2.0.dev0" git push origin preview # publishes nothing # 3. tag and push — this is the release event -git tag v3.2.0.dev0 -git push origin v3.2.0.dev0 +git tag 3.2.0.dev0 +git push origin 3.2.0.dev0 ``` Verify: @@ -110,8 +111,8 @@ gh pr merge --merge # 4. tag master git checkout master && git pull grep numerapi_version setup.py # must read exactly 3.2.0 -git tag v3.2.0 -git push origin v3.2.0 +git tag 3.2.0 +git push origin 3.2.0 # 5. keep preview caught up so it does not drift git checkout preview && git merge master && git push origin preview @@ -135,7 +136,7 @@ git checkout -b hotfix/3.2.1 master # branch off master, NOT preview gh pr create --base master # after merge: git checkout master && git pull -git tag v3.2.1 && git push origin v3.2.1 +git tag 3.2.1 && git push origin 3.2.1 git checkout preview && git merge master && git push origin preview ``` @@ -145,7 +146,7 @@ Read the Docs is fully automatic — there is nothing to tag or move. - `/en/latest/` tracks `master`. - `/en/stable/` tracks the greatest **non-pre-release** semver tag, so cutting - `v3.2.0` promotes it; `v3.2.0.dev0` is correctly ignored. + `3.2.0` promotes it; `3.2.0.dev0` is correctly ignored. Do not create a tag or branch named `stable`. That overrides the automatic behavior above, has to be force-moved by hand on every release, and silently goes @@ -156,13 +157,15 @@ matters, cut a patch release. ## What CI enforces -`.github/workflows/pypi.yml` runs on tag pushes matching `v[0-9]*` and will -refuse to publish unless: +`.github/workflows/pypi.yml` runs on tag pushes that start with a digit (and on +`v`-prefixed tags, solely to reject them). It refuses to publish unless: -1. **The tag matches `setup.py`.** Compared as normalized PEP 440 versions, so - `v3.2.0dev0` and `v3.2.0.dev0` are equivalent, but `v3.2.0` against a - `setup.py` of `3.2.0.dev0` fails. -2. **Final releases point at a commit on `master`.** Pre-releases skip this +1. **The tag has no `v` prefix.** `v3.2.0` fails with an error telling you to + re-tag as `3.2.0`. +2. **The tag matches `setup.py`.** Compared as normalized PEP 440 versions, so + `3.2.0dev0` and `3.2.0.dev0` are equivalent, but `3.2.0` against a `setup.py` + of `3.2.0.dev0` fails. +3. **Final releases point at a commit on `master`.** Pre-releases skip this check, so betas can be cut from `preview` but a final one cannot. `pytest.yml` (Python 3.10–3.14) and `ruff.yml` run on every push and PR. @@ -175,14 +178,14 @@ release on PyPI either. **Tag mismatch error.** You tagged without bumping `setup.py`, or vice versa. Fix `setup.py`, commit, delete the tag locally and on origin -(`git push origin :refs/tags/vX.Y.Z`), then re-tag. Deleting a tag never -publishes anything. +(`git push origin :refs/tags/X.Y.Z`), then re-tag. Deleting a tag never publishes +anything. **"Final release tags must point at a commit on master."** You tagged a suffix-free version on `preview`. Either merge to `master` first, or cut it as a `.devN` pre-release instead. -**Do not retro-tag old releases.** Any new tag matching `v[0-9]*` triggers a +**Do not retro-tag old releases.** Any new tag starting with a digit triggers a publish attempt that will fail on a duplicate version. Historical tags are inconsistent (some `v`-prefixed, some not, several `.devN` tags that published final versions before the guards existed) — leave them as they are. diff --git a/setup.py b/setup.py index 514fe2e..3370a33 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ def load(path): return open(path, "r").read() -numerapi_version = "3.1.2.dev0" +numerapi_version = "3.1.2.dev1" classifiers = [ "Development Status :: 5 - Production/Stable", From 3cad6dc993eb4483f387098b0b2ef93114bfe7ae Mon Sep 17 00:00:00 2001 From: Noah Harasz Date: Tue, 11 Aug 2026 14:19:39 -0700 Subject: [PATCH 07/11] T-374 Cut over performance payout multipliers --- CHANGELOG.md | 2 ++ docs/round-score-configs.md | 13 +++++++------ numerapi/base_api.py | 19 +++++++++++++++---- tests/test_base_api.py | 27 +++++++++++++++++++++++++-- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9623222..3d26681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Notable changes to this project. ## [3.1.2] - Unreleased +- replace deprecated `round_model_performances_v2` Corr/MMC multiplier fields + with the exact `payoutMultipliers` list - support Python 3.14 and pandas 3: require `pandas>=2.3.3` on Python 3.14 and keep `pandas>=1.1.0` below it - declare `python_requires>=3.10` and advertise Python 3.10 through 3.14 diff --git a/docs/round-score-configs.md b/docs/round-score-configs.md index bac0613..de040aa 100644 --- a/docs/round-score-configs.md +++ b/docs/round-score-configs.md @@ -71,9 +71,10 @@ different scores into Corr or MMC roles. ## Deprecated performance endpoint `round_model_performances_v2()` remains an isolated deprecated compatibility -method. Its `corrMultiplier` and `mmcMultiplier` fields come from the deprecated -`v2RoundModelPerformances` GraphQL endpoint and must not be used to infer score -identity. Use `submission_scores()` for identity-preserving score results and -join them to `list_rounds()` by round when payout configuration is needed. -Neither performance method nor `list_rounds()` has a dedicated CLI command, so -there is no CLI return shape to migrate. +method. It returns the GraphQL endpoint's exact `payoutMultipliers` list; each +entry includes round-score-config and score-config IDs, name, version, display +name, and multiplier. The legacy `corrMultiplier` and `mmcMultiplier` keys no +longer exist. Use `submission_scores()` when payout configuration is not needed, +or join results to `list_rounds()` by round. Neither performance method nor +`list_rounds()` has a dedicated CLI command, so there is no CLI return shape to +migrate. diff --git a/numerapi/base_api.py b/numerapi/base_api.py index afde839..f8d2d0f 100644 --- a/numerapi/base_api.py +++ b/numerapi/base_api.py @@ -1405,8 +1405,13 @@ def round_model_performances_v2(self, model_id: str): content: * atRisk (`float`) - * corrMultiplier (`float` or None) - * mmcMultiplier (`float` or None) + * payoutMultipliers (`list`): exact payout score configurations + * id (`str`): round score configuration ID + * scoreConfigId (`str`): score configuration ID + * name (`str`) + * version (`str`) + * displayName (`str`) + * multiplier (`float`) * roundPayoutFactor (`float` or None) * roundNumber (`int`) * roundOpenTime (`datetime`) @@ -1436,8 +1441,14 @@ def round_model_performances_v2(self, model_id: str): v2RoundModelPerformances(modelId: $modelId tournament: $tournament) { atRisk - corrMultiplier, - mmcMultiplier, + payoutMultipliers { + id + scoreConfigId + name + version + displayName + multiplier + } roundPayoutFactor, roundNumber, roundOpenTime, diff --git a/tests/test_base_api.py b/tests/test_base_api.py index d10622a..8f67cb9 100644 --- a/tests/test_base_api.py +++ b/tests/test_base_api.py @@ -572,8 +572,16 @@ def test_round_model_performances_v2_warns(api): "v2RoundModelPerformances": [ { "atRisk": "10.5", - "corrMultiplier": 1.0, - "mmcMultiplier": 0.5, + "payoutMultipliers": [ + { + "id": "round-score-config-id", + "scoreConfigId": "score-config-id", + "name": "unfamiliar_score", + "version": "7", + "displayName": "unfamiliar", + "multiplier": 0.5, + } + ], "roundPayoutFactor": "0.8", "roundNumber": 456, "roundOpenTime": "2026-03-01T00:00:00Z", @@ -605,6 +613,21 @@ def test_round_model_performances_v2_warns(api): assert isinstance(res[0]["roundOpenTime"], datetime.datetime) assert isinstance(res[0]["roundResolveTime"], datetime.datetime) assert res[0]["submissionScores"][0]["payoutPending"] == decimal.Decimal("0.8") + assert res[0]["payoutMultipliers"] == [ + { + "id": "round-score-config-id", + "scoreConfigId": "score-config-id", + "name": "unfamiliar_score", + "version": "7", + "displayName": "unfamiliar", + "multiplier": 0.5, + } + ] + + query = json.loads(responses.calls[0].request.body)["query"] + assert "payoutMultipliers" in query + assert "corrMultiplier" not in query + assert "mmcMultiplier" not in query @responses.activate From 45e13306cb8645ec2352258c09b93119ed7f85be Mon Sep 17 00:00:00 2001 From: Josh Terrill Date: Tue, 11 Aug 2026 14:30:55 -0700 Subject: [PATCH 08/11] updated version to 3.1.0.dev0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3370a33..f3a6cba 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ def load(path): return open(path, "r").read() -numerapi_version = "3.1.2.dev1" +numerapi_version = "3.1.0.dev0" classifiers = [ "Development Status :: 5 - Production/Stable", From 85ba7c6d0ed9c5d2656743b60d1d7d06ef1c20f6 Mon Sep 17 00:00:00 2001 From: Josh Terrill Date: Tue, 11 Aug 2026 14:31:30 -0700 Subject: [PATCH 09/11] updated version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f3a6cba..247caa0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ def load(path): return open(path, "r").read() -numerapi_version = "3.1.0.dev0" +numerapi_version = "3.1.0.dev1" classifiers = [ "Development Status :: 5 - Production/Stable", From 8ef93a1fbabd2a1496997f28d825838ee80d6004 Mon Sep 17 00:00:00 2001 From: Josh Terrill Date: Wed, 12 Aug 2026 13:51:36 -0700 Subject: [PATCH 10/11] updated date on changelog, increment version to release version in setup.py --- CHANGELOG.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d26681..722cc85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog Notable changes to this project. -## [3.1.2] - Unreleased +## [3.1.2] - 2026-08-12 - replace deprecated `round_model_performances_v2` Corr/MMC multiplier fields with the exact `payoutMultipliers` list - support Python 3.14 and pandas 3: require `pandas>=2.3.3` on Python 3.14 and diff --git a/setup.py b/setup.py index 247caa0..a94d7b2 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ def load(path): return open(path, "r").read() -numerapi_version = "3.1.0.dev1" +numerapi_version = "3.1.0" classifiers = [ "Development Status :: 5 - Production/Stable", From c8f21e66dee53bfd1d8604482e636b6cdbad11be Mon Sep 17 00:00:00 2001 From: Josh Terrill Date: Wed, 12 Aug 2026 14:24:20 -0700 Subject: [PATCH 11/11] updated changelog with correct version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 722cc85..9da0fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog Notable changes to this project. -## [3.1.2] - 2026-08-12 +## [3.1.0] - 2026-08-12 - replace deprecated `round_model_performances_v2` Corr/MMC multiplier fields with the exact `payoutMultipliers` list - support Python 3.14 and pandas 3: require `pandas>=2.3.3` on Python 3.14 and