From b1130c4131024fe14f6f905e92f54f0e67a301c4 Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Mon, 21 Sep 2026 13:48:24 +0000 Subject: [PATCH] chalkpy-rs: Add version 0.1.1 --- .github/workflows/build-chalkpy-rs.yml | 268 +++++++++++++++++++++++++ docs/packages/chalkpy-rs.yaml | 5 + 2 files changed, 273 insertions(+) create mode 100644 .github/workflows/build-chalkpy-rs.yml create mode 100644 docs/packages/chalkpy-rs.yaml diff --git a/.github/workflows/build-chalkpy-rs.yml b/.github/workflows/build-chalkpy-rs.yml new file mode 100644 index 0000000000..f867cb7935 --- /dev/null +++ b/.github/workflows/build-chalkpy-rs.yml @@ -0,0 +1,268 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# chalkpy-rs has no public source repository and no public CI: Chalk AI publishes +# only the PyPI sdist, which carries the whole Cargo workspace, so the sdist is the +# upstream tree and is fetched here. Everything else mirrors upstream's packaging: +# setuptools-rust over the root Cargo.toml with the `python` feature, PyO3 without +# abi3, one wheel per interpreter. +name: Build chalkpy-rs wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'Version glob to (re)build; empty builds every version of docs/packages/chalkpy-rs.yaml not released yet' + required: false + default: '' + pull_request: + branches: [main] + paths: + - '.github/workflows/build-chalkpy-rs.yml' + - 'docs/packages/chalkpy-rs.yaml' + push: + branches: [main] + paths: + - '.github/workflows/build-chalkpy-rs.yml' + - 'docs/packages/chalkpy-rs.yaml' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + with: + package: chalkpy-rs + version: ${{ inputs.version }} + + build_wheels: + needs: [setup] + if: needs.setup.outputs.versions != '[]' + name: Build chalkpy-rs ${{ matrix.version }} ${{ matrix.python }}-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 300 + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + # No cp314t: upstream ships cp310-cp314 only, all cp3x-cp3x. + python: ["cp312", "cp313", "cp314"] + + env: + CHALKPY_RS_VERSION: ${{ matrix.version }} + + steps: + - name: Fetch chalkpy-rs ${{ env.CHALKPY_RS_VERSION }} sdist + run: | + url="$(curl -sSfL "https://pypi.org/pypi/chalkpy-rs/${CHALKPY_RS_VERSION}/json" \ + | python3 -c 'import json,sys; print(next(u["url"] for u in json.load(sys.stdin)["urls"] if u["packagetype"] == "sdist"))')" + curl -sSfL -o sdist.tar.gz "$url" + # Extracted rather than passed to cibuildwheel as a tarball: for a + # tarball package-dir cibuildwheel chdirs into its own extraction temp + # dir, and CIBW_TEST_SOURCES resolves against that cwd, so the staged + # tests below would be invisible. + tar xzf sdist.tar.gz + test -f "chalkpy_rs-${CHALKPY_RS_VERSION}/pyproject.toml" + + - name: Stage smoke tests + run: | + mkdir -p tests + cat > tests/test_chalk_rs.py <<'PYTEST' + """Smoke tests for the riscv64 chalkpy-rs wheel. + + Upstream ships no Python test suite, so these exercise the compiled PyO3 + extension end to end: the duration/ISO-8601 helpers, the naming helpers and + the ruff-backed Python AST indexer that is the bulk of the extension. + """ + + import datetime + import textwrap + + import pytest + + import chalk_rs + + FEATURES_SOURCE = textwrap.dedent( + ''' + from chalk.features import features, feature + from chalk import online + + + @features + class User: + id: str + email: str = feature(description="primary email", owner="team@example.com", tags=["pii"]) + age: int + + + @online + def get_age(uid: User.id) -> User.age: + return 42 + ''' + ).lstrip() + + + @pytest.fixture + def project(tmp_path): + path = tmp_path / "features.py" + path.write_text(FEATURES_SOURCE) + return str(tmp_path), [str(path)] + + + def test_extension_module_is_compiled(): + assert chalk_rs.__file__.endswith(".so"), chalk_rs.__file__ + + + def test_parse_duration(): + assert chalk_rs.parse_duration_ms("1h") == 3_600_000 + assert chalk_rs.parse_duration_ms("1h30m") == 5_400_000 + assert chalk_rs.parse_duration_s("1d") == 86_400 + with pytest.raises(ValueError): + chalk_rs.parse_duration_s("not a duration") + + + def test_seconds_to_duration_string(): + assert chalk_rs.seconds_to_duration_string(3661.0) == "1h1m1s" + assert chalk_rs.seconds_to_duration_string(90.0) == "1m30s" + + + def test_parse_iso_date_time(): + assert chalk_rs.parse_iso_date("2026-09-21") == datetime.date(2026, 9, 21) + assert chalk_rs.parse_iso_time("13:45:06") == datetime.time(13, 45, 6) + assert chalk_rs.parse_datetime("2026-09-21T13:45:06") == datetime.datetime(2026, 9, 21, 13, 45, 6) + aware = chalk_rs.parse_datetime("2026-09-21T13:45:06+00:00") + assert aware.utcoffset() == datetime.timedelta(0) + with pytest.raises(ValueError): + chalk_rs.parse_iso_date("not a date") + + + def test_iso_duration_round_trip(): + delta = datetime.timedelta(days=1, hours=2, minutes=3, seconds=4) + assert chalk_rs.parse_iso_duration("P1DT2H3M4S") == delta + assert chalk_rs.duration_isoformat(delta) == "P1DT2H3M4S" + with pytest.raises(TypeError): + chalk_rs.duration_isoformat(123) + + + def test_timezone_from_name(): + assert chalk_rs.timezone_from_name("UTC") is not None + assert chalk_rs.timezone_from_name("Not/AZone") is None + assert datetime.datetime( + 2026, 9, 21, tzinfo=chalk_rs.timezone_from_name("UTC") + ).utcoffset() == datetime.timedelta(0) + + + def test_naming_helpers(): + assert chalk_rs.to_snake_case("FooBarBaz") == "foo_bar_baz" + assert chalk_rs.to_snake_case("HTTPResponse") == "http_response" + assert chalk_rs.build_namespaced_name("user", "email") == "user::email" + assert chalk_rs.build_namespaced_name(None, "email") == "email" + + + def test_file_system_and_parser_cache(project): + root, files = project + filesystem = chalk_rs.StdAstFileSystem(files) + assert filesystem.all_files() == files + assert filesystem.read_to_string(files[0]) == FEATURES_SOURCE + + cache = chalk_rs.AstFileParserCache(files, root) + path, source, imports = cache.get_parsed_file(files[0]) + assert path == files[0] + assert source == FEATURES_SOURCE + assert imports["chalk.features"]["feature"] == ["feature"] + assert imports["chalk"]["online"] == ["online"] + + + def test_feature_class_ast(project): + root, files = project + index = chalk_rs.AstProjectIndex(files, root) + index.nonblocking_start_index() + + feature_class = index.feature_class_ast_in_file(files[0], "User") + assert feature_class is not None + assert feature_class.class_name == "User" + assert feature_class.namespace == "user" + assert feature_class.module == "features" + assert sorted(feature_class.fields) == ["age", "email", "id"] + + email = feature_class.fields["email"] + assert email.field_name == "email" + assert sorted(email.kwarg_names) == ["description", "owner", "tags"] + line, start, end_line, end = email.field_name_location + assert line == end_line + assert FEATURES_SOURCE.splitlines()[line][start:end] == "email" + + + def test_resolver_ast(project): + root, files = project + index = chalk_rs.AstProjectIndex(files, root) + resolver = index.resolver_ast_in_file(files[0], "get_age") + assert resolver is not None + assert resolver.resolver_name == "get_age" + assert resolver.module == "features" + assert resolver.args_in_order == ["uid"] + assert resolver.return_annotation is not None + assert resolver.missing_return_annotation is None + assert index.resolver_ast_in_file(files[0], "does_not_exist") is None + PYTEST + + - name: Build wheels + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + output-dir: wheelhouse/ + package-dir: chalkpy_rs-${{ env.CHALKPY_RS_VERSION }} + # musllinux is dropped: rustup.rs ships no riscv64 musl host toolchain. + only: ${{ matrix.python }}-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # No [tool.cibuildwheel] table upstream, so the Rust toolchain + # setuptools-rust needs is installed in-container here. + CIBW_BEFORE_ALL_LINUX: >- + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + CIBW_ENVIRONMENT_LINUX: >- + PATH="$PATH:$HOME/.cargo/bin" + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + CIBW_TEST_REQUIRES: pytest + CIBW_TEST_SOURCES: tests + CIBW_TEST_COMMAND: python -m pytest -v tests + + - name: Check the wheel ships the compiled extension + run: | + python3 - wheelhouse/*.whl <<'EOF' + import sys, zipfile + names = zipfile.ZipFile(sys.argv[1]).namelist() + sos = [n for n in names if n.endswith(".so")] + assert len(sos) == 1, names + print("extension:", sos[0]) + EOF + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: chalkpy-rs-${{ env.CHALKPY_RS_VERSION }}-${{ matrix.python }}-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish chalkpy-rs ${{ matrix.version }} + needs: [setup, build_wheels] + if: needs.setup.outputs.versions != '[]' + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.setup.outputs.versions) }} + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + secrets: + app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }} + with: + artifact-pattern: chalkpy-rs-${{ matrix.version }}-*-manylinux_riscv64 diff --git a/docs/packages/chalkpy-rs.yaml b/docs/packages/chalkpy-rs.yaml new file mode 100644 index 0000000000..43b569e0fb --- /dev/null +++ b/docs/packages/chalkpy-rs.yaml @@ -0,0 +1,5 @@ +package-name: chalkpy-rs +source-code: https://pypi.org/project/chalkpy-rs/ +license: Unknown +versions: +- version: 0.1.1