Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,23 @@ jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
# Two legs, the two ends of `requires-python = ">=3.11"` (#283):
# - "3.11" is the FLOOR, stated explicitly because nothing else states it: the suite
# passing there is what keeps the declared floor a measured claim instead of a hope.
# 3.10 fails collection on `typing.assert_never` (3.11+), which is the one concrete
# feature binding the floor.
# - "3.14" is the leg local development actually runs (`.python-version` pins 3.14.4);
# it is named here by major only, since the matrix needs SOME explicit version and
# "whatever .python-version says" is not addressable per-leg.
# Both legs stay inside THIS job so the required status context remains `test` -- the
# matrix fans out the context, it does not rename it, so every leg gates the merge.
# ONE leg, because the floor and the development interpreter are now the same version.
#
# This was two legs -- "3.11" and "3.14" -- the two ends of the `>=3.11` floor #283 chose.
# With `requires-python = ">=3.14"` there is no span left to test across: the floor leg and
# the development leg would be the same interpreter, and a matrix of one is a matrix that
# costs a second checkout to prove nothing.
#
# It stays a matrix rather than a bare `python-version:` deliberately. When a second
# supported version arrives -- 3.15, or a floor drop -- adding it here is one list entry,
# and the required status context stays `test` either way, because the matrix fans the
# context out without renaming it. Collapsing to a scalar would make that a workflow edit.
#
# "3.14" is named by major.minor only; `.python-version` pins the exact patch (3.14.4) for
# local work, and "whatever .python-version says" is not addressable per-leg.
strategy:
matrix:
python: ["3.11", "3.14"]
python: ["3.14"]
steps:
- uses: actions/checkout@v7

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ jobs:
run: |
set -euo pipefail
# Must be the PINNED interpreter, not the runner's default `python`. The wheel
# accepts `Requires-Python: >=3.11` (every pyproject.toml in the workspace says
# so, pinned by tests/test_python_floor.py), so a default-python venv would
# accepts `Requires-Python: >=3.14` (every pyproject.toml in the workspace says
# so, pinned by tests/test_python_floor.py), so a default-python venv could still
# INSTALL -- and verify the artifact on an interpreter nobody tested or shipped:
# the release is built and typed against .python-version, and the verify step
# must exercise that same one. --seed provides the pip that the install below
Expand Down
19 changes: 14 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,20 @@ source, is written up in [`docs/fiqh-basis.md`](docs/fiqh-basis.md).

## Development setup and the gates a PR must pass

Python **3.11+** is the floor, and it is measured, not aspirational (#283): the full suite
passes identically on 3.11–3.14, and the one feature binding the floor is 3.11's
`typing.assert_never` (3.10 fails collection on it). The repo itself develops on 3.14
(`.python-version`; `uv python install 3.14`), and CI runs the whole gate on 3.11 too, so the
floor cannot silently rot.
Python **3.14+** is the floor, and it is a policy floor, not a measured one — the distinction
matters if you are about to argue with it. The suite passes identically on 3.11–3.14; nothing
here needs 3.14. The floor was 3.11 for exactly that reason (#283), and went up because keel now
ships a signed desktop bundle and an installer that bootstrap their own interpreter, so an end
user's system Python is no longer what keel runs on. The floor now reaches contributors and
packagers rather than everyone who installs.

It also buys two things that were blocked at 3.11: `ruff` can target `py314` (under the old
floor `ruff format` could emit PEP 758 syntax that 3.11–3.13 cannot parse), and the `numpy.*`
mypy override is gone. `uv python install 3.14` if you do not have it; `.python-version` pins
the exact patch. CI runs the whole gate on 3.14, so the floor cannot silently rot.

If the floor blocks you, say so in an issue — "a contributor was actually blocked" is the
argument that would lower it again, and there is no feature standing in the way.
Then:

```bash
Expand Down
2 changes: 1 addition & 1 deletion README.ar.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Platform (CDP) مجانيًّا للقراءة فقط** — تُجلَب بيا

```bash
git clone https://github.com/CodeGateSoftware/keel.git && cd keel
uv sync --all-extras --dev # أي Python من 3.11 فما فوق (المستودع يُطوَّر على 3.14)
uv sync --all-extras --dev # Python 3.14 فما فوق (`.python-version` يثبّت الإصدار الدقيق)
cp .env.example .env # ضع مفتاح وسرّ CDP فيه — لبيانات السوق فقط
uv run keel rules seed # سجّل عائلات القواعد كمرشّحات
uv run keel fetch # اجلب تاريخ الشموع للقائمة المسموحة الافتراضية
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ so upfront rather than let step 4 be a surprise.

```bash
git clone https://github.com/CodeGateSoftware/keel.git && cd keel
uv sync --all-extras --dev # any Python 3.11+ (the repo develops on 3.14)
uv sync --all-extras --dev # Python 3.14+ (`.python-version` pins the patch)
cp .env.example .env # put the CDP key/secret in it — market data only
uv run keel rules seed # register the rule families as candidates
uv run keel fetch # pull candle history for the default allowlist
Expand Down
2 changes: 1 addition & 1 deletion docs/desktop-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ curl -fsSL https://raw.githubusercontent.com/CodeGateSoftware/keel/main/scripts/
You do not have to trust that line blind: the script is
[`scripts/install.sh`](../scripts/install.sh) in this repository, written to be read — every step
prints what it is about to do before it does it, it runs no privileged commands, and it verifies
itself with `keel versions` before claiming success. It needs a terminal and Python 3.11 or
itself with `keel versions` before claiming success. It needs a terminal and Python 3.14 or
later. To build from a source checkout instead, see the README's **"Try it in five minutes"**.

Both are exactly the friction the desktop app exists to remove, so this is not the recommendation
Expand Down
3 changes: 2 additions & 1 deletion docs/launch.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ the last box, in the order given, and never before.
the document as a plan, not a claim (#318/#289).
- [x] **The Arabic entry point** — `README.ar.md` with the switcher, terminology exact,
scope stated (#319/#290).
- [x] **CI green on `main`** — both matrix legs (3.11, 3.14; the legs came in #301), and
- [x] **CI green on `main`** — the matrix leg (3.14; two legs, 3.11 and 3.14, came in #301 and
collapsed to one when the floor rose to 3.14), and
the merge gate (`test` context) has been the required context since #268.
- [x] **The code-quality scans actually configured** — tokenless and always on
(#320): Dependabot over every manifest, a weekly `pip-audit` over the exported lock,
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-broker-alpaca/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "keel-broker-alpaca"
version = "0.11.2"
description = "Alpaca Trading API adapter for keel (US equities, cash account)"
license = "Apache-2.0"
requires-python = ">=3.11"
requires-python = ">=3.14"
# Siblings pinned `==` (see the root `pyproject.toml`); `requests` matches the
# `keel-broker-robinhood` transport convention -- raw REST over an injected transport,
# deliberately NO `alpaca-py` SDK: Alpaca's Trading + Market Data APIs are plain
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-broker-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "keel-broker-api"
version = "0.11.2"
description = "Broker port, domain types, capability model, and conformance suite for keel"
license = "Apache-2.0"
requires-python = ">=3.11"
requires-python = ">=3.14"
# Pinned `==`: see the note in the root `pyproject.toml`. An unpinned sibling is satisfied by
# whatever is already installed, which is how a deployment ends up running mixed versions.
dependencies = ["keel-core==0.11.2"]
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-broker-coinbase/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "keel-broker-coinbase"
version = "0.11.2"
description = "Coinbase Advanced Trade adapter for keel"
license = "Apache-2.0"
requires-python = ">=3.11"
requires-python = ">=3.14"
# Siblings pinned `==` (see the root `pyproject.toml`); the third-party SDK is not, because it
# has its own release cycle and is not cut from this repo.
dependencies = ["keel-core==0.11.2", "keel-broker-api==0.11.2", "coinbase-advanced-py>=1.8.4"]
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-broker-fake/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "keel-broker-fake"
version = "0.11.2"
description = "A deliberately divergent second broker adapter, to keep the port honest"
license = "Apache-2.0"
requires-python = ">=3.11"
requires-python = ">=3.14"
# Pinned `==`: see the note in the root `pyproject.toml`.
dependencies = ["keel-core==0.11.2", "keel-broker-api==0.11.2"]

Expand Down
2 changes: 1 addition & 1 deletion packages/keel-broker-kraken/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "keel-broker-kraken"
version = "0.11.2"
description = "Kraken adapter for keel (port-complete stub; every data/market method raises)"
license = "Apache-2.0"
requires-python = ">=3.11"
requires-python = ">=3.14"
# Siblings pinned `==` (see the root `pyproject.toml`). There is deliberately NO third-party
# dependency: this package is a stub (#313) that handles no keys and makes no network calls,
# so a Kraken SDK (and its credentials) arrive with the real implementation, not before it.
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-broker-robinhood/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "keel-broker-robinhood"
version = "0.11.2"
description = "Robinhood Crypto Trading API v2 adapter for keel"
license = "Apache-2.0"
requires-python = ">=3.11"
requires-python = ">=3.14"
# `pynacl` is here and nowhere else in the workspace: Robinhood signs every request with an
# Ed25519 key, which no other venue keel talks to requires. Keeping it a dependency of this
# package alone means an engine that never installs this adapter never installs the crypto stack
Expand Down
2 changes: 1 addition & 1 deletion packages/keel-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "keel-core"
version = "0.11.2"
description = "Shared domain types, configuration, and logging for keel"
license = "Apache-2.0"
requires-python = ">=3.11"
requires-python = ">=3.14"
dependencies = [
"pyyaml>=6.0.3",
"python-dotenv>=1.2.2",
Expand Down
2 changes: 1 addition & 1 deletion packaging/macos_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ and choosing Open no longer works as a shortcut -- Apple removed that path delib
PREFER NO WARNING AT ALL?

Install from the release wheels instead. Nothing is downloaded as an application, so
nothing objects -- but it needs a terminal and Python 3.11 or later:
nothing objects -- but it needs a terminal and Python 3.14 or later:

pip install --find-links . ./keel_trader-<version>-py3-none-any.whl

Expand Down
16 changes: 2 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ readme = "README.md"
authors = [
{ name = "CodeGate Software" }
]
requires-python = ">=3.11"
requires-python = ">=3.14"
dependencies = [
"click>=8.4.2",
# pyyaml/python-dotenv are deliberately NOT listed here: nothing under `keel/` imports
Expand Down Expand Up @@ -124,7 +124,7 @@ dev = [
# [tool.ruff] here, so keeping a copy in both places would leave silently-dead settings.

[tool.mypy]
python_version = "3.11"
python_version = "3.14"
files = ["packages", "keel", "tests"]
# Implied by `--strict`, but mypy rejects it in a per-module section ("Per-module sections
# should only specify per-module flags"), so it is stated here rather than in the broker block
Expand Down Expand Up @@ -192,18 +192,6 @@ ignore_missing_imports = true
module = ["keel_core.*"]
ignore_errors = true

# numpy rides in with optuna (the #476 research extra, dev group only) and is reached only
# by the ONE lazy `import optuna` inside keel/research/tuning.py's runner -- nothing under
# keel/ imports numpy itself. numpy's bundled stubs use PEP 695 `type` statements, which
# this run's python_version=3.11 floor cannot parse, so the transitive crawl is silenced
# here rather than by raising the floor every package in this workspace pins.
[[tool.mypy.overrides]]
module = ["numpy.*"]
follow_imports = "skip"
# `follow_imports` alone ignores .pyi files by design; numpy ships BUNDLED STUBS whose
# PEP 695 `type` statements cannot parse under the python_version=3.11 floor above, so the
# skip must reach the stubs too.
follow_imports_for_stubs = true

# Coverage is measured only when `--cov` is passed (see the pytest-cov note in [dependency-groups]).
# This section exists so that a bare `--cov` -- with no value -- measures the right thing instead of
Expand Down
16 changes: 10 additions & 6 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
line-length = 100
# Must not exceed the `requires-python` floor every pyproject in this workspace declares
# (`>=3.11`). It said `py314`, and that is not cosmetic: `ruff format` rewrote a
# `except (OverflowError, OSError, ValueError):` into PEP 758's unparenthesised form, which is a
# SyntaxError on every interpreter this project claims to support. A formatter that can silently
# emit syntax the package cannot run is worse than no formatter. Pinned by
# `tests/test_packaging.py`.
target-version = "py311"
# (`>=3.14`). The rule is unchanged and still pinned by `tests/test_packaging.py`; what changed
# is the floor.
#
# This said `py311` for a concrete reason, now spent: under `py314` against a `>=3.11` floor,
# `ruff format` rewrote `except (OverflowError, OSError, ValueError):` into PEP 758's
# unparenthesised form -- a SyntaxError on 3.11, 3.12 and 3.13, all of which the metadata then
# invited. A formatter that can silently emit syntax the package cannot run is worse than no
# formatter. With the floor at 3.14 there is no interpreter left that cannot parse it, so the
# hazard is gone rather than merely tolerated, and `py314` is the honest target.
target-version = "py314"

[lint]
select = ["E", "F", "I", "UP"]
Expand Down
15 changes: 10 additions & 5 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,23 @@ command -v curl >/dev/null 2>&1 || die "curl is required but was not found on PA

# -- step 2/7: Python, with the floor stated ------------------------------------------------------

# keel requires Python 3.11+ (tests/test_python_floor.py); the check is the interpreter's
# keel requires Python 3.14+ (tests/test_python_floor.py); the check is the interpreter's
# own version_info, not a parsed string, and the failure names the floor and what to do.
say "step 2/7: finding Python >= 3.11"
#
# The candidate list keeps `python3` first and then names 3.14 explicitly: on a machine where
# `python3` is an older system Python, the loop must still find a newer one installed alongside
# it rather than stopping at the first interpreter it can execute. Versions below the floor are
# no longer listed -- naming them would only produce a candidate the check must then reject.
say "step 2/7: finding Python >= 3.14"
PY=""
for candidate in python3 python3.14 python3.13 python3.12 python3.11; do
for candidate in python3 python3.14; do
command -v "$candidate" >/dev/null 2>&1 || continue
if "$candidate" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)' 2>/dev/null; then
if "$candidate" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 14) else 1)' 2>/dev/null; then
PY="$(command -v "$candidate")"
break
fi
done
[ -n "$PY" ] || die "no Python >= 3.11 found on PATH -- keel requires 3.11 or later. Check
[ -n "$PY" ] || die "no Python >= 3.14 found on PATH -- keel requires 3.14 or later. Check
'python3 --version', install a newer Python, and re-run this script."
say " ok: ${PY} ($("$PY" -c 'import platform; print(platform.python_version())'))"

Expand Down
2 changes: 1 addition & 1 deletion tests/test_desktop_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def test_the_lockfile_is_checked_before_anything_can_mutate_it(

def test_the_verify_step_comment_states_the_real_python_floor() -> None:
"""#438: the verify step's comment claimed the wheel carries `Requires-Python: >=3.14.4`
while every pyproject.toml declares `>=3.11` (pinned by tests/test_python_floor.py).
while every pyproject.toml declares `>=3.14` (pinned by tests/test_python_floor.py).

Harmless to the run -- the pinned interpreter is used either way -- but the comment was
the only place a reader could learn what the wheel demands, and the next person to build
Expand Down
28 changes: 21 additions & 7 deletions tests/test_install_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import re
import stat
import tomllib
from pathlib import Path

import pytest
Expand Down Expand Up @@ -108,14 +109,27 @@ def test_no_credentials_or_auth(script: str) -> None:


def test_the_python_floor_is_checked_and_stated(script: str) -> None:
"""The interpreter's own `version_info` must be compared against (3, 11) -- the
floor every distribution declares (tests/test_python_floor.py) -- and the failure
must NAME the floor, because a bare 'wrong python' sends the user nowhere."""
assert re.search(r"sys\.version_info >= \(3, ?11\)", script), (
"the >= 3.11 check via sys.version_info is gone -- the installer would happily "
"build a venv keel refuses to run in"
"""The interpreter's own `version_info` must be compared against the floor every
distribution declares, and the failure must NAME it -- a bare 'wrong python' sends the
user nowhere.

The floor is DERIVED from `requires-python` rather than written here, because this is the
one place it is stated in shell rather than in metadata, and shell is not resolved by pip.
Hardcoded, a raised floor leaves the installer building a venv keel then refuses to run in,
and the mismatch surfaces as an import error after a successful-looking install.
"""
root = tomllib.loads((_ROOT / "pyproject.toml").read_text())
requires = root["project"]["requires-python"]
assert requires.startswith(">="), requires
floor = requires[2:].strip()
major, minor = (int(part) for part in floor.split("."))

assert re.search(rf"sys\.version_info >= \({major}, ?{minor}\)", script), (
f"the >= {floor} check via sys.version_info is gone or has drifted from "
f"requires-python ({requires!r}) -- the installer would build a venv keel refuses "
"to run in"
)
assert "3.11" in script, "the failure message must state the floor"
assert floor in script, "the failure message must state the floor"


# -- the wheels: exactly the five, never by name from an index -----------------------------------
Expand Down
2 changes: 1 addition & 1 deletion tests/test_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ def test_ruffs_target_version_does_not_exceed_the_python_floor():
except (OverflowError, OSError, ValueError):

into PEP 758's unparenthesised form -- which is a SyntaxError on 3.11, 3.12 and 3.13, all of
which `requires-python = ">=3.11"` promises to support. The wheel still builds and still
which the `>=3.11` floor of the day promised to support. The wheel still builds and still
installs; it fails at import, on the interpreters the metadata invited. A formatter that can
silently emit syntax the package cannot run is worse than no formatter, and the mismatch is
invisible unless something pins it.
Expand Down
Loading
Loading