From c32726ab794149415b297d84ae09869074c2acb8 Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Wed, 8 Apr 2026 18:07:59 +0900 Subject: [PATCH 01/10] Add polars release packaging and CI workflows --- .github/workflows/polars-ci.yml | 74 ++++++++ .github/workflows/polars-release.yml | 243 +++++++++++++++++++++++++++ Makefile | 8 +- core/Cargo.toml | 5 + polars/Cargo.toml | 10 +- polars/README.md | 33 +++- polars/polars_techr/__init__.py | 4 +- polars/pyproject.toml | 35 +++- polars/scripts/check_artifacts.py | 113 +++++++++++++ polars/scripts/check_versions.py | 82 +++++++++ polars/scripts/smoke_import.py | 20 +++ polars/uv.lock | 3 +- 12 files changed, 616 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/polars-ci.yml create mode 100644 .github/workflows/polars-release.yml create mode 100644 polars/scripts/check_artifacts.py create mode 100644 polars/scripts/check_versions.py create mode 100644 polars/scripts/smoke_import.py diff --git a/.github/workflows/polars-ci.yml b/.github/workflows/polars-ci.yml new file mode 100644 index 0000000..81fec8b --- /dev/null +++ b/.github/workflows/polars-ci.yml @@ -0,0 +1,74 @@ +name: Polars CI + +on: + pull_request: + paths: + - ".github/workflows/polars-*.yml" + - "Cargo.toml" + - "Makefile" + - "core/**" + - "polars/**" + push: + branches: + - main + paths: + - ".github/workflows/polars-*.yml" + - "Cargo.toml" + - "Makefile" + - "core/**" + - "polars/**" + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@v5 + - uses: dtolnay/rust-toolchain@stable + + - name: Sync polars dependencies + working-directory: polars + run: uv sync --group dev + + - name: Check package versions + run: python polars/scripts/check_versions.py + + - name: Test techr-core + run: cargo test -p techr-core + + - name: Build local extension for tests + working-directory: polars + run: uv run maturin develop --uv + + - name: Test polars package + working-directory: polars + run: uv run pytest + + - name: Build wheel and sdist + working-directory: polars + run: uv run maturin build --release --sdist --out dist + + - name: Check artifact contents + run: python polars/scripts/check_artifacts.py polars/dist + + - name: Smoke test built wheel + run: | + wheel="$(python - <<'PY' + from pathlib import Path + + wheels = sorted(Path('polars/dist').glob('*.whl')) + if len(wheels) != 1: + raise SystemExit(f"expected exactly one wheel, found {len(wheels)}") + print(wheels[0].resolve().as_posix()) + PY + )" + uv run --isolated --python 3.13 --with "$wheel" python polars/scripts/smoke_import.py diff --git a/.github/workflows/polars-release.yml b/.github/workflows/polars-release.yml new file mode 100644 index 0000000..52a72a9 --- /dev/null +++ b/.github/workflows/polars-release.yml @@ -0,0 +1,243 @@ +name: Polars Release + +on: + push: + tags: + - "polars-v*" + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-release: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Check release versions + run: | + if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then + python polars/scripts/check_versions.py "${GITHUB_REF_NAME}" + else + python polars/scripts/check_versions.py + fi + + build-linux: + needs: check-release + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - target: x86_64 + artifact: wheels-linux-x86_64 + - target: aarch64 + artifact: wheels-linux-aarch64 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@v5 + - name: Build wheel + uses: PyO3/maturin-action@v1 + with: + working-directory: polars + target: ${{ matrix.target }} + manylinux: "2014" + sccache: "true" + args: --release --out dist + + - name: Check artifact contents + run: python polars/scripts/check_artifacts.py polars/dist + + - name: Smoke test x86_64 wheel + if: matrix.target == 'x86_64' + run: | + wheel="$(python - <<'PY' + from pathlib import Path + + wheels = sorted(Path('polars/dist').glob('*.whl')) + if len(wheels) != 1: + raise SystemExit(f"expected exactly one wheel, found {len(wheels)}") + print(wheels[0].resolve().as_posix()) + PY + )" + uv run --isolated --python 3.13 --with "$wheel" python polars/scripts/smoke_import.py + + - name: Smoke test aarch64 wheel + if: matrix.target == 'aarch64' + uses: uraimo/run-on-arch-action@v2 + with: + arch: aarch64 + distro: ubuntu22.04 + githubToken: ${{ github.token }} + install: | + apt-get update + apt-get install -y --no-install-recommends python3 python3-pip + pip3 install -U pip + run: | + set -e + pip3 install polars/dist/*.whl --force-reinstall + python3 polars/scripts/smoke_import.py + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact }} + path: polars/dist/* + + build-macos: + needs: check-release + runs-on: macos-14 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@v5 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-apple-darwin + - name: Build universal2 wheel + uses: PyO3/maturin-action@v1 + with: + working-directory: polars + sccache: "true" + args: --release --out dist --universal2 + + - name: Check artifact contents + run: python polars/scripts/check_artifacts.py polars/dist + + - name: Smoke test built wheel + run: | + wheel="$(python - <<'PY' + from pathlib import Path + + wheels = sorted(Path('polars/dist').glob('*.whl')) + if len(wheels) != 1: + raise SystemExit(f"expected exactly one wheel, found {len(wheels)}") + print(wheels[0].resolve().as_posix()) + PY + )" + uv run --isolated --python 3.13 --with "$wheel" python polars/scripts/smoke_import.py + + - uses: actions/upload-artifact@v4 + with: + name: wheels-macos-universal2 + path: polars/dist/* + + build-windows: + needs: check-release + runs-on: windows-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + architecture: "x64" + - uses: astral-sh/setup-uv@v5 + - name: Build wheel + uses: PyO3/maturin-action@v1 + with: + working-directory: polars + target: x64 + sccache: "true" + args: --release --out dist + + - name: Check artifact contents + run: python polars/scripts/check_artifacts.py polars/dist + + - name: Smoke test built wheel + run: | + wheel="$(python - <<'PY' + from pathlib import Path + + wheels = sorted(Path('polars/dist').glob('*.whl')) + if len(wheels) != 1: + raise SystemExit(f"expected exactly one wheel, found {len(wheels)}") + print(wheels[0].resolve().as_posix()) + PY + )" + uv run --isolated --python 3.13 --with "$wheel" python polars/scripts/smoke_import.py + + - uses: actions/upload-artifact@v4 + with: + name: wheels-windows-x64 + path: polars/dist/* + + build-sdist: + needs: check-release + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + working-directory: polars + command: sdist + args: --out dist + + - name: Check artifact contents + run: python polars/scripts/check_artifacts.py polars/dist + + - uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: polars/dist/* + + publish-testpypi: + if: github.event_name == 'workflow_dispatch' + needs: [build-linux, build-macos, build-windows, build-sdist] + runs-on: ubuntu-latest + environment: testpypi + permissions: + contents: read + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: dist + skip-existing: true + + publish-pypi: + if: github.event_name == 'push' + needs: [build-linux, build-macos, build-windows, build-sdist] + runs-on: ubuntu-latest + environment: pypi + permissions: + contents: read + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist diff --git a/Makefile b/Makefile index cbf128b..b981a92 100644 --- a/Makefile +++ b/Makefile @@ -6,14 +6,15 @@ export CARGO_TERM_COLOR=$(shell (test -t 0 && echo "always") || echo "auto") .PHONY: build-dev-polars build-dev-polars: - @rm -f polars/polars_techr/*.so + @rm -f polars/polars_techr/*.so polars/polars_techr/*.pyd cd polars && uv run maturin develop --uv .PHONY: build-prod-polars build-prod-polars: - @rm -f polars/polars_techr/*.so - cd polars && uv run maturin build --release + @rm -f polars/polars_techr/*.so polars/polars_techr/*.pyd + @rm -rf polars/dist + cd polars && uv run maturin build --release --sdist --out dist .PHONY: test-core @@ -22,6 +23,7 @@ test-core: .PHONY: test-polars test-polars: + cd polars && uv run maturin develop --uv cd polars && uv run pytest .PHONY: test diff --git a/core/Cargo.toml b/core/Cargo.toml index d2aec3d..30b8896 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -2,6 +2,11 @@ name = "techr-core" version = "0.1.0" edition = "2021" +description = "Rust core implementation for techr technical indicators" +license = "MIT" +readme = "../README.md" +homepage = "https://github.com/alphaprime-dev/techr" +repository = "https://github.com/alphaprime-dev/techr" [lib] name = "techr" diff --git a/polars/Cargo.toml b/polars/Cargo.toml index 9f170b4..2d69068 100644 --- a/polars/Cargo.toml +++ b/polars/Cargo.toml @@ -2,9 +2,17 @@ name = "polars_techr" version = "0.1.0" edition = "2021" +description = "Polars expression plugins for techr indicators" +license = "MIT" +readme = "README.md" +homepage = "https://github.com/alphaprime-dev/techr" +repository = "https://github.com/alphaprime-dev/techr" + +[lib] +crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.22.2", features = ["extension-module", "abi3-py38"] } +pyo3 = { version = "0.22.2", features = ["extension-module", "abi3-py310"] } pyo3-polars = { version = "0.17.0", features = ["derive"] } serde = { version = "1", features = ["derive"] } polars = { version = "0.43.1", default-features = false } diff --git a/polars/README.md b/polars/README.md index 48961b5..42df97d 100644 --- a/polars/README.md +++ b/polars/README.md @@ -1,6 +1,12 @@ # polars-techr -Python wrapper for `techr` indicators on top of Polars plugins. +`polars-techr` exposes `techr` indicators as Polars expression plugins. + +## Installation + +```bash +uv add polars-techr +``` ## Supported indicators @@ -46,3 +52,28 @@ result = df.select( - `ichimoku_leading_span_b` uses `period` for the rolling window and `base_line_period` for the forward displacement. The Python wrapper defaults `base_line_period` to `26`. - `ichimoku_lagging_span` uses `base_line_period` for its backward displacement. - Polars plugins keep the output row-aligned with the input, so `ichimoku_leading_span_a` and `ichimoku_leading_span_b` truncate the forward-projected tail from the core result. +## Development + +```bash +cd polars +uv sync --group dev +uv run maturin develop --uv +uv run pytest +``` + +Build distributable artifacts locally with: + +```bash +cd polars +uv run maturin build --release --sdist --out dist +uv run python scripts/check_artifacts.py dist +``` + +## Release + +1. Update the version in `Cargo.toml`. +2. Verify metadata and artifact inputs with `uv run python scripts/check_versions.py`. +3. Optionally run the `Polars Release` workflow manually to publish the current ref to TestPyPI. +4. Create and push a `polars-vX.Y.Z` tag to publish to PyPI. + +Before the first release, configure Trusted Publishers for both PyPI and TestPyPI on `alphaprime-dev/techr`. diff --git a/polars/polars_techr/__init__.py b/polars/polars_techr/__init__.py index 9287dd7..5113d5d 100644 --- a/polars/polars_techr/__init__.py +++ b/polars/polars_techr/__init__.py @@ -4,9 +4,9 @@ import polars as pl from polars.plugins import register_plugin_function -from polars_techr.types import IntoExpr +from .types import IntoExpr -LIB = Path(__file__).parent +LIB = Path(__file__).resolve().parent __all__ = [ "bband_lower", diff --git a/polars/pyproject.toml b/polars/pyproject.toml index 15a6e1f..a23e776 100644 --- a/polars/pyproject.toml +++ b/polars/pyproject.toml @@ -1,12 +1,28 @@ [project] name = "polars-techr" -version = "0.1.0" -description = "Python wrapper for polars-techr" +dynamic = ["version"] +description = "Polars expression plugins for techr indicators" authors = [{ name = "alphaprime-dev" }] requires-python = ">=3.10,<3.14" readme = "README.md" -license = "MIT" -dependencies = ["polars>=1.26.0"] +license = { text = "MIT" } +dependencies = ["polars>=1.39,<2"] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Rust", + "Topic :: Scientific/Engineering", +] + +[project.urls] +Homepage = "https://github.com/alphaprime-dev/techr" +Repository = "https://github.com/alphaprime-dev/techr" +Issues = "https://github.com/alphaprime-dev/techr/issues" [dependency-groups] dev = [ @@ -21,7 +37,16 @@ default-groups = "all" [tool.maturin] module-name = "polars_techr._polars_techr" +python-packages = ["polars_techr"] +strip = true +exclude = [ + ".pytest_cache/**", + ".ruff_cache/**", + ".venv/**", + "polars_techr/__pycache__/**", + "tests/__pycache__/**", +] [build-system] -requires = ["maturin>=1.0,<2.0", "polars>=1.7.1"] +requires = ["maturin>=1.7.1,<2"] build-backend = "maturin" diff --git a/polars/scripts/check_artifacts.py b/polars/scripts/check_artifacts.py new file mode 100644 index 0000000..bcf982c --- /dev/null +++ b/polars/scripts/check_artifacts.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import argparse +import tarfile +import zipfile +from pathlib import Path, PurePosixPath + + +BANNED_PARTS = { + "__pycache__", + ".pytest_cache", + ".ruff_cache", + ".venv", +} +BANNED_SUFFIXES = {".pyc", ".pyo"} +REQUIRED_WHEEL_FILES = { + PurePosixPath("polars_techr/__init__.py"), + PurePosixPath("polars_techr/types.py"), +} +REQUIRED_SDIST_SUFFIXES = { + "pyproject.toml", + "Cargo.toml", + "README.md", + "polars_techr/__init__.py", + "polars_techr/types.py", + "src/lib.rs", + "src/expressions.rs", +} + + +def load_members(path: Path) -> list[PurePosixPath]: + if path.suffix == ".whl": + with zipfile.ZipFile(path) as archive: + return [PurePosixPath(name) for name in archive.namelist()] + if path.suffixes[-2:] == [".tar", ".gz"]: + with tarfile.open(path, "r:gz") as archive: + return [PurePosixPath(name) for name in archive.getnames()] + raise ValueError(f"Unsupported artifact format: {path}") + + +def strip_root(paths: list[PurePosixPath]) -> list[PurePosixPath]: + stripped: list[PurePosixPath] = [] + for path in paths: + if len(path.parts) <= 1: + continue + stripped.append(PurePosixPath(*path.parts[1:])) + return stripped + + +def assert_no_banned_entries(path: Path, members: list[PurePosixPath]) -> None: + for member in members: + if any(part in BANNED_PARTS for part in member.parts): + raise ValueError(f"{path.name} contains banned path: {member.as_posix()}") + if member.suffix in BANNED_SUFFIXES: + raise ValueError(f"{path.name} contains banned file: {member.as_posix()}") + + +def validate_wheel(path: Path, members: list[PurePosixPath]) -> None: + required_missing = sorted( + file.as_posix() for file in REQUIRED_WHEEL_FILES if file not in members + ) + if required_missing: + raise ValueError(f"{path.name} is missing files: {', '.join(required_missing)}") + + native_extensions = [ + member + for member in members + if member.parent == PurePosixPath("polars_techr") + and member.name.startswith("_polars_techr.") + and member.suffix in {".so", ".pyd"} + ] + if len(native_extensions) != 1: + found = ", ".join(member.as_posix() for member in native_extensions) or "none" + raise ValueError( + f"{path.name} must contain exactly one native extension, found {found}" + ) + + +def validate_sdist(path: Path, members: list[PurePosixPath]) -> None: + normalized = [member.as_posix() for member in strip_root(members)] + missing = sorted( + name for name in REQUIRED_SDIST_SUFFIXES if not any( + member == name or member.endswith(f"/{name}") for member in normalized + ) + ) + if missing: + raise ValueError(f"{path.name} is missing files: {', '.join(missing)}") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("artifact_dir", type=Path) + args = parser.parse_args() + + artifact_dir = args.artifact_dir.resolve() + artifacts = sorted(artifact_dir.glob("*.whl")) + sorted(artifact_dir.glob("*.tar.gz")) + if not artifacts: + raise SystemExit(f"No artifacts found in {artifact_dir}") + + for artifact in artifacts: + members = load_members(artifact) + assert_no_banned_entries(artifact, members) + if artifact.suffix == ".whl": + validate_wheel(artifact, members) + else: + validate_sdist(artifact, members) + print(f"checked {artifact.name}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/polars/scripts/check_versions.py b/polars/scripts/check_versions.py new file mode 100644 index 0000000..f92c830 --- /dev/null +++ b/polars/scripts/check_versions.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +TAG_PREFIX = "polars-v" + + +def read_section_version(path: Path, section: str) -> str: + text = path.read_text() + section_pattern = re.compile( + rf"(?ms)^\[{re.escape(section)}\]\s*(.*?)(?=^\[|\Z)" + ) + section_match = section_pattern.search(text) + if section_match is None: + raise ValueError(f"Could not find [{section}] in {path}") + + version_pattern = re.compile(r'(?m)^version\s*=\s*"([^"]+)"\s*$') + version_match = version_pattern.search(section_match.group(1)) + if version_match is None: + raise ValueError(f"Could not find version in [{section}] of {path}") + + return version_match.group(1) + + +def require_project_dynamic_version(path: Path) -> None: + text = path.read_text() + section_pattern = re.compile(r"(?ms)^\[project\]\s*(.*?)(?=^\[|\Z)") + section_match = section_pattern.search(text) + if section_match is None: + raise ValueError(f"Could not find [project] in {path}") + + dynamic_pattern = re.compile(r'(?m)^dynamic\s*=\s*\[(.*?)\]\s*$') + dynamic_match = dynamic_pattern.search(section_match.group(1)) + if dynamic_match is None or '"version"' not in dynamic_match.group(1): + raise ValueError( + f"{path} must declare dynamic = [\"version\"] in [project]" + ) + + +def normalize_tag(tag: str) -> str: + if tag.startswith("refs/tags/"): + tag = tag.removeprefix("refs/tags/") + return tag + + +def main() -> int: + polars_dir = Path(__file__).resolve().parents[1] + pyproject_path = polars_dir / "pyproject.toml" + require_project_dynamic_version(pyproject_path) + cargo_version = read_section_version(polars_dir / "Cargo.toml", "package") + + if len(sys.argv) > 2: + print("Usage: check_versions.py [polars-vX.Y.Z]", file=sys.stderr) + return 1 + + if len(sys.argv) == 2: + tag = normalize_tag(sys.argv[1]) + if not tag.startswith(TAG_PREFIX): + print( + f"Expected a tag starting with {TAG_PREFIX!r}, got {tag!r}.", + file=sys.stderr, + ) + return 1 + tag_version = tag.removeprefix(TAG_PREFIX) + if tag_version != cargo_version: + print( + "Tag version mismatch:", + f"tag={tag_version}", + f"package={cargo_version}", + file=sys.stderr, + ) + return 1 + + print(f"polars-techr version: {cargo_version}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/polars/scripts/smoke_import.py b/polars/scripts/smoke_import.py new file mode 100644 index 0000000..1d9643f --- /dev/null +++ b/polars/scripts/smoke_import.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from pathlib import Path + +import polars as pl +import polars_techr as ta + + +def main() -> int: + expr = ta.sma(pl.col("x"), period=2) + if not isinstance(expr, pl.Expr): + raise TypeError("polars-techr did not return a Polars expression") + + print(f"loaded {Path(ta.__file__).resolve()}") + print(expr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/polars/uv.lock b/polars/uv.lock index d976286..f65d594 100644 --- a/polars/uv.lock +++ b/polars/uv.lock @@ -100,7 +100,6 @@ wheels = [ [[package]] name = "polars-techr" -version = "0.1.0" source = { editable = "." } dependencies = [ { name = "polars" }, @@ -115,7 +114,7 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "polars", specifier = ">=1.26.0" }] +requires-dist = [{ name = "polars", specifier = ">=1.39,<2" }] [package.metadata.requires-dev] dev = [ From f24f6c5e6bd937e2ee574db8e80ef677bc7531c1 Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Wed, 8 Apr 2026 18:19:45 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20redundant?= =?UTF-8?q?=20polars=20version=20check=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/polars-ci.yml | 3 - .github/workflows/polars-release.yml | 22 -------- polars/README.md | 2 +- polars/scripts/check_versions.py | 82 ---------------------------- 4 files changed, 1 insertion(+), 108 deletions(-) delete mode 100644 polars/scripts/check_versions.py diff --git a/.github/workflows/polars-ci.yml b/.github/workflows/polars-ci.yml index 81fec8b..4a28251 100644 --- a/.github/workflows/polars-ci.yml +++ b/.github/workflows/polars-ci.yml @@ -39,9 +39,6 @@ jobs: working-directory: polars run: uv sync --group dev - - name: Check package versions - run: python polars/scripts/check_versions.py - - name: Test techr-core run: cargo test -p techr-core diff --git a/.github/workflows/polars-release.yml b/.github/workflows/polars-release.yml index 52a72a9..70d0c2b 100644 --- a/.github/workflows/polars-release.yml +++ b/.github/workflows/polars-release.yml @@ -10,26 +10,7 @@ permissions: contents: read jobs: - check-release: - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - name: Check release versions - run: | - if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then - python polars/scripts/check_versions.py "${GITHUB_REF_NAME}" - else - python polars/scripts/check_versions.py - fi - build-linux: - needs: check-release runs-on: ubuntu-latest strategy: fail-fast: false @@ -96,7 +77,6 @@ jobs: path: polars/dist/* build-macos: - needs: check-release runs-on: macos-14 defaults: run: @@ -139,7 +119,6 @@ jobs: path: polars/dist/* build-windows: - needs: check-release runs-on: windows-latest defaults: run: @@ -181,7 +160,6 @@ jobs: path: polars/dist/* build-sdist: - needs: check-release runs-on: ubuntu-latest defaults: run: diff --git a/polars/README.md b/polars/README.md index 42df97d..627b5c0 100644 --- a/polars/README.md +++ b/polars/README.md @@ -72,7 +72,7 @@ uv run python scripts/check_artifacts.py dist ## Release 1. Update the version in `Cargo.toml`. -2. Verify metadata and artifact inputs with `uv run python scripts/check_versions.py`. +2. Optionally build release artifacts locally for a final preflight check. 3. Optionally run the `Polars Release` workflow manually to publish the current ref to TestPyPI. 4. Create and push a `polars-vX.Y.Z` tag to publish to PyPI. diff --git a/polars/scripts/check_versions.py b/polars/scripts/check_versions.py deleted file mode 100644 index f92c830..0000000 --- a/polars/scripts/check_versions.py +++ /dev/null @@ -1,82 +0,0 @@ -from __future__ import annotations - -import re -import sys -from pathlib import Path - - -TAG_PREFIX = "polars-v" - - -def read_section_version(path: Path, section: str) -> str: - text = path.read_text() - section_pattern = re.compile( - rf"(?ms)^\[{re.escape(section)}\]\s*(.*?)(?=^\[|\Z)" - ) - section_match = section_pattern.search(text) - if section_match is None: - raise ValueError(f"Could not find [{section}] in {path}") - - version_pattern = re.compile(r'(?m)^version\s*=\s*"([^"]+)"\s*$') - version_match = version_pattern.search(section_match.group(1)) - if version_match is None: - raise ValueError(f"Could not find version in [{section}] of {path}") - - return version_match.group(1) - - -def require_project_dynamic_version(path: Path) -> None: - text = path.read_text() - section_pattern = re.compile(r"(?ms)^\[project\]\s*(.*?)(?=^\[|\Z)") - section_match = section_pattern.search(text) - if section_match is None: - raise ValueError(f"Could not find [project] in {path}") - - dynamic_pattern = re.compile(r'(?m)^dynamic\s*=\s*\[(.*?)\]\s*$') - dynamic_match = dynamic_pattern.search(section_match.group(1)) - if dynamic_match is None or '"version"' not in dynamic_match.group(1): - raise ValueError( - f"{path} must declare dynamic = [\"version\"] in [project]" - ) - - -def normalize_tag(tag: str) -> str: - if tag.startswith("refs/tags/"): - tag = tag.removeprefix("refs/tags/") - return tag - - -def main() -> int: - polars_dir = Path(__file__).resolve().parents[1] - pyproject_path = polars_dir / "pyproject.toml" - require_project_dynamic_version(pyproject_path) - cargo_version = read_section_version(polars_dir / "Cargo.toml", "package") - - if len(sys.argv) > 2: - print("Usage: check_versions.py [polars-vX.Y.Z]", file=sys.stderr) - return 1 - - if len(sys.argv) == 2: - tag = normalize_tag(sys.argv[1]) - if not tag.startswith(TAG_PREFIX): - print( - f"Expected a tag starting with {TAG_PREFIX!r}, got {tag!r}.", - file=sys.stderr, - ) - return 1 - tag_version = tag.removeprefix(TAG_PREFIX) - if tag_version != cargo_version: - print( - "Tag version mismatch:", - f"tag={tag_version}", - f"package={cargo_version}", - file=sys.stderr, - ) - return 1 - - print(f"polars-techr version: {cargo_version}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From 2b1d6dac6a109b406e6318a22b7df883ee67d1d2 Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Wed, 8 Apr 2026 18:27:39 +0900 Subject: [PATCH 03/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Reduce=20polars=20CI?= =?UTF-8?q?=20trigger=20frequency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/polars-ci.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/polars-ci.yml b/.github/workflows/polars-ci.yml index 4a28251..640ff11 100644 --- a/.github/workflows/polars-ci.yml +++ b/.github/workflows/polars-ci.yml @@ -2,21 +2,17 @@ name: Polars CI on: pull_request: + types: [review_requested, ready_for_review] paths: - ".github/workflows/polars-*.yml" - "Cargo.toml" - "Makefile" - "core/**" - "polars/**" - push: - branches: - - main - paths: - - ".github/workflows/polars-*.yml" - - "Cargo.toml" - - "Makefile" - - "core/**" - - "polars/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true permissions: contents: read From a64f087784c2ec3704a455488ca25be40f683525 Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Mon, 13 Apr 2026 11:06:47 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=94=A7=20Use=20full=20Windows=20tar?= =?UTF-8?q?get=20triple=20in=20polars=20release=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/polars-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/polars-release.yml b/.github/workflows/polars-release.yml index 70d0c2b..f287853 100644 --- a/.github/workflows/polars-release.yml +++ b/.github/workflows/polars-release.yml @@ -134,7 +134,7 @@ jobs: uses: PyO3/maturin-action@v1 with: working-directory: polars - target: x64 + target: x86_64-pc-windows-msvc sccache: "true" args: --release --out dist From ade90a0700258b50ef6ac3b1b397d9434b4bdb9b Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Mon, 13 Apr 2026 11:21:23 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=93=9D=20Add=20missing=20newline=20?= =?UTF-8?q?in=20polars=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- polars/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/polars/README.md b/polars/README.md index 627b5c0..61c2171 100644 --- a/polars/README.md +++ b/polars/README.md @@ -52,6 +52,7 @@ result = df.select( - `ichimoku_leading_span_b` uses `period` for the rolling window and `base_line_period` for the forward displacement. The Python wrapper defaults `base_line_period` to `26`. - `ichimoku_lagging_span` uses `base_line_period` for its backward displacement. - Polars plugins keep the output row-aligned with the input, so `ichimoku_leading_span_a` and `ichimoku_leading_span_b` truncate the forward-projected tail from the core result. + ## Development ```bash From 8e110638e53146952a1e5ad5b60d315fe2b1e2b9 Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Mon, 13 Apr 2026 11:26:27 +0900 Subject: [PATCH 06/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Relax=20sdist=20sour?= =?UTF-8?q?ce=20checks=20in=20artifact=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- polars/scripts/check_artifacts.py | 43 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/polars/scripts/check_artifacts.py b/polars/scripts/check_artifacts.py index bcf982c..ce77977 100644 --- a/polars/scripts/check_artifacts.py +++ b/polars/scripts/check_artifacts.py @@ -17,14 +17,11 @@ PurePosixPath("polars_techr/__init__.py"), PurePosixPath("polars_techr/types.py"), } -REQUIRED_SDIST_SUFFIXES = { - "pyproject.toml", - "Cargo.toml", - "README.md", - "polars_techr/__init__.py", - "polars_techr/types.py", - "src/lib.rs", - "src/expressions.rs", +REQUIRED_SDIST_FILES = { + PurePosixPath("pyproject.toml"), + PurePosixPath("Cargo.toml"), + PurePosixPath("README.md"), + PurePosixPath("polars_techr/__init__.py"), } @@ -77,14 +74,30 @@ def validate_wheel(path: Path, members: list[PurePosixPath]) -> None: def validate_sdist(path: Path, members: list[PurePosixPath]) -> None: - normalized = [member.as_posix() for member in strip_root(members)] - missing = sorted( - name for name in REQUIRED_SDIST_SUFFIXES if not any( - member == name or member.endswith(f"/{name}") for member in normalized - ) + normalized = strip_root(members) + missing_files = sorted( + file.as_posix() for file in REQUIRED_SDIST_FILES if file not in normalized + ) + if missing_files: + raise ValueError(f"{path.name} is missing files: {', '.join(missing_files)}") + + required_patterns = { + "polars_techr/*.py": any( + member.parent == PurePosixPath("polars_techr") and member.suffix == ".py" + for member in normalized + ), + "src/*.rs": any( + member.parent == PurePosixPath("src") and member.suffix == ".rs" + for member in normalized + ), + } + missing_patterns = sorted( + pattern for pattern, matched in required_patterns.items() if not matched ) - if missing: - raise ValueError(f"{path.name} is missing files: {', '.join(missing)}") + if missing_patterns: + raise ValueError( + f"{path.name} is missing required source patterns: {', '.join(missing_patterns)}" + ) def main() -> int: From 292b7a46612d6ed7c0ef7a3f67c041789ffd10e1 Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Mon, 13 Apr 2026 12:53:51 +0900 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20environm?= =?UTF-8?q?ents=20from=20polars=20release=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/polars-release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/polars-release.yml b/.github/workflows/polars-release.yml index f287853..c883f7e 100644 --- a/.github/workflows/polars-release.yml +++ b/.github/workflows/polars-release.yml @@ -188,7 +188,6 @@ jobs: if: github.event_name == 'workflow_dispatch' needs: [build-linux, build-macos, build-windows, build-sdist] runs-on: ubuntu-latest - environment: testpypi permissions: contents: read id-token: write @@ -207,7 +206,6 @@ jobs: if: github.event_name == 'push' needs: [build-linux, build-macos, build-windows, build-sdist] runs-on: ubuntu-latest - environment: pypi permissions: contents: read id-token: write From 448ecadf848c819e10be7ca9790756fad77f1ed4 Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Mon, 13 Apr 2026 13:20:01 +0900 Subject: [PATCH 08/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Validate=20polars=20?= =?UTF-8?q?workflows=20with=20Python=203.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/polars-ci.yml | 4 ++-- .github/workflows/polars-release.yml | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/polars-ci.yml b/.github/workflows/polars-ci.yml index 640ff11..6f75b88 100644 --- a/.github/workflows/polars-ci.yml +++ b/.github/workflows/polars-ci.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.10" - uses: astral-sh/setup-uv@v5 - uses: dtolnay/rust-toolchain@stable @@ -64,4 +64,4 @@ jobs: print(wheels[0].resolve().as_posix()) PY )" - uv run --isolated --python 3.13 --with "$wheel" python polars/scripts/smoke_import.py + uv run --isolated --python 3.10 --with "$wheel" python polars/scripts/smoke_import.py diff --git a/.github/workflows/polars-release.yml b/.github/workflows/polars-release.yml index c883f7e..35fb111 100644 --- a/.github/workflows/polars-release.yml +++ b/.github/workflows/polars-release.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.10" - uses: astral-sh/setup-uv@v5 - name: Build wheel uses: PyO3/maturin-action@v1 @@ -36,7 +36,7 @@ jobs: target: ${{ matrix.target }} manylinux: "2014" sccache: "true" - args: --release --out dist + args: --release --out dist -i python3.10 - name: Check artifact contents run: python polars/scripts/check_artifacts.py polars/dist @@ -53,7 +53,7 @@ jobs: print(wheels[0].resolve().as_posix()) PY )" - uv run --isolated --python 3.13 --with "$wheel" python polars/scripts/smoke_import.py + uv run --isolated --python 3.10 --with "$wheel" python polars/scripts/smoke_import.py - name: Smoke test aarch64 wheel if: matrix.target == 'aarch64' @@ -85,7 +85,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.10" - uses: astral-sh/setup-uv@v5 - uses: dtolnay/rust-toolchain@stable with: @@ -111,7 +111,7 @@ jobs: print(wheels[0].resolve().as_posix()) PY )" - uv run --isolated --python 3.13 --with "$wheel" python polars/scripts/smoke_import.py + uv run --isolated --python 3.10 --with "$wheel" python polars/scripts/smoke_import.py - uses: actions/upload-artifact@v4 with: @@ -127,7 +127,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.10" architecture: "x64" - uses: astral-sh/setup-uv@v5 - name: Build wheel @@ -152,7 +152,7 @@ jobs: print(wheels[0].resolve().as_posix()) PY )" - uv run --isolated --python 3.13 --with "$wheel" python polars/scripts/smoke_import.py + uv run --isolated --python 3.10 --with "$wheel" python polars/scripts/smoke_import.py - uses: actions/upload-artifact@v4 with: @@ -168,7 +168,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: "3.10" - name: Build sdist uses: PyO3/maturin-action@v1 with: From 37161d11e41fe9e7701d13fa981312ca54cf2946 Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Mon, 13 Apr 2026 13:29:05 +0900 Subject: [PATCH 09/10] =?UTF-8?q?=F0=9F=90=9B=20Fix=20nested=20sdist=20sou?= =?UTF-8?q?rce=20path=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- polars/scripts/check_artifacts.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/polars/scripts/check_artifacts.py b/polars/scripts/check_artifacts.py index ce77977..398fe82 100644 --- a/polars/scripts/check_artifacts.py +++ b/polars/scripts/check_artifacts.py @@ -44,6 +44,10 @@ def strip_root(paths: list[PurePosixPath]) -> list[PurePosixPath]: return stripped +def has_suffix_path(path: PurePosixPath, suffix: tuple[str, ...]) -> bool: + return len(path.parts) >= len(suffix) and path.parts[-len(suffix) :] == suffix + + def assert_no_banned_entries(path: Path, members: list[PurePosixPath]) -> None: for member in members: if any(part in BANNED_PARTS for part in member.parts): @@ -83,11 +87,14 @@ def validate_sdist(path: Path, members: list[PurePosixPath]) -> None: required_patterns = { "polars_techr/*.py": any( - member.parent == PurePosixPath("polars_techr") and member.suffix == ".py" + member.suffix == ".py" + and len(member.parts) >= 2 + and member.parts[-2] == "polars_techr" for member in normalized ), "src/*.rs": any( - member.parent == PurePosixPath("src") and member.suffix == ".rs" + member.suffix == ".rs" + and has_suffix_path(member.parent, ("src",)) for member in normalized ), } From bd021f3481f4f791d77a6da01c5120e868661249 Mon Sep 17 00:00:00 2001 From: Minki Kim Date: Mon, 13 Apr 2026 13:32:36 +0900 Subject: [PATCH 10/10] =?UTF-8?q?=E2=9A=A1=20Skip=20project=20install=20du?= =?UTF-8?q?ring=20polars=20CI=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/polars-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/polars-ci.yml b/.github/workflows/polars-ci.yml index 6f75b88..771ce2e 100644 --- a/.github/workflows/polars-ci.yml +++ b/.github/workflows/polars-ci.yml @@ -33,7 +33,7 @@ jobs: - name: Sync polars dependencies working-directory: polars - run: uv sync --group dev + run: uv sync --group dev --no-install-project - name: Test techr-core run: cargo test -p techr-core