diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4e46ef9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2026 The Pybricks Authors + +name: Release + +# One package per tag, because the packages are released independently: the +# tag pybricks-core-v1.0.0 publishes pybricks-core 1.0.0 and nothing else. +# Tag pybricks-core first, then pybricks-client and pybricks-service, then +# pybricks-cli, so that each one's requirements are on PyPI before it is. +on: + push: + tags: ["pybricks-*-v*"] + +permissions: + contents: read + +env: + UV_FROZEN: "1" + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: { persist-credentials: false } + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + # A tag that disagrees with the package it names is refused before + # anything is built. A version on PyPI can never be uploaded again, so a + # mistake caught after the upload is a version number lost. + - name: Check the tag against the package + shell: bash + env: + TAG: ${{ github.ref_name }} + run: | + package="${TAG%-v*}" + version="${TAG##*-v}" + dir="packages/pb-${package#pybricks-}" + test -f "$dir/pyproject.toml" || { echo "::error::$TAG names no package"; exit 1; } + actual="$(uv version --package "$package" --short)" + test "$actual" = "$version" || { echo "::error::$TAG, but $package is $actual"; exit 1; } + grep -qF "## [$version]" "$dir/CHANGELOG.md" || { echo "::error::$dir/CHANGELOG.md has no [$version] section"; exit 1; } + echo "PACKAGE=$package" >> "$GITHUB_ENV" + - run: uv build --package "$PACKAGE" --out-dir dist + - run: uvx twine check --strict dist/* + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: { name: dist, path: dist/ } + + publish: + needs: build + # The PyPI trusted publisher for each package names this workflow and this + # environment, so a token is never stored anywhere. + environment: + name: pypi + permissions: + id-token: write # OIDC, which is how PyPI knows the upload is from here + runs-on: ubuntu-24.04 + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: { name: dist, path: dist } + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - run: uv publish --trusted-publishing always dist/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a42a14..3d2cfcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The format is based on - A uv workspace of four packages, `src` layout throughout, with `py.typed`. - ruff, pyright strict, pytest and a strict docs build, gated from commit one. -- CI on Linux, macOS, Windows and Windows ARM64, across Python 3.11 to 3.13. +- CI on Linux, macOS, Windows and Windows ARM64, across Python 3.11 to 3.14. - Documentation with the API and command references generated from the code. ### Notes diff --git a/README.md b/README.md index a5f9ced..d67c2d7 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,6 @@ async with pb_client.connect() as pb: await pb.run_program(await pb_client.compile_script(Path("my_robot.py"))) ``` -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. [Documentation](docs/index.md) · [Contributing](CONTRIBUTING.md) diff --git a/docs/index.md b/docs/index.md index 3e95a96..c9d0ed4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ Run Python programs on your [Pybricks](https://pybricks.com) hub from your computer — from the command line, or from your own Python code. !!! note "Status" - Early development. Nothing here is stable yet. + Beta. Commands and APIs may still change before 1.0.0. ## Install diff --git a/packages/pb-cli/CHANGELOG.md b/packages/pb-cli/CHANGELOG.md index c498b65..1554710 100644 --- a/packages/pb-cli/CHANGELOG.md +++ b/packages/pb-cli/CHANGELOG.md @@ -25,3 +25,4 @@ Notable changes to `pybricks-cli`, the `pb` command. The format is based on - Data on stdout and everything else on stderr, so every command can be piped. - One-line errors with a suggested fix; a traceback only when it is our bug. - An arrow-key picker wherever the tool would otherwise have to guess. +- `--transport usb`, `ble` or `rfcomm`, the last for EV3 and NXT hubs. diff --git a/packages/pb-cli/README.md b/packages/pb-cli/README.md index b0111f2..35ad17f 100644 --- a/packages/pb-cli/README.md +++ b/packages/pb-cli/README.md @@ -20,4 +20,4 @@ starts instantly, and so does the hundredth. `pb repl` talks to the hub's interpreter a line at a time, and `pb run -c` runs a line or two of Python without a file. `pb --help` lists the rest. -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. diff --git a/packages/pb-cli/pyproject.toml b/packages/pb-cli/pyproject.toml index 7bceb0d..1605639 100644 --- a/packages/pb-cli/pyproject.toml +++ b/packages/pb-cli/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pybricks-cli" -version = "0.1.0" +version = "1.0.0b1" description = "The Pybricks command line interface" readme = "README.md" license = "MIT" @@ -10,6 +10,7 @@ maintainers = [{ name = "David Lechner", email = "david@pybricks.com" }] requires-python = ">=3.11" keywords = ["pybricks", "lego", "micropython", "bluetooth", "robotics"] classifiers = [ + "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Education", @@ -19,9 +20,9 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "pybricks-core", - "pybricks-client", - "pybricks-service", + "pybricks-core>=1.0.0b1", + "pybricks-client>=1.0.0b1", + "pybricks-service>=1.0.0b1", "anyio>=4.14", "cyclopts>=4.22", "rich>=13.7", @@ -35,7 +36,7 @@ pb = "pb_cli.cli:main" [project.urls] homepage = "https://pybricks.com" repository = "https://github.com/pybricks/pybricks-tools" -documentation = "https://pybricks.github.io/pybricks-tools/cli/" +documentation = "https://pybricks.com/pybricks-tools/cli/" "Changelog" = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-cli/CHANGELOG.md" "Support" = "https://github.com/pybricks/support/discussions" "Issues" = "https://github.com/pybricks/support/issues" diff --git a/packages/pb-cli/src/pb_cli/__init__.py b/packages/pb-cli/src/pb_cli/__init__.py index 0456c02..cee311b 100644 --- a/packages/pb-cli/src/pb_cli/__init__.py +++ b/packages/pb-cli/src/pb_cli/__init__.py @@ -3,6 +3,6 @@ """The Pybricks command line interface.""" -__version__ = "0.1.0" +__version__ = "1.0.0b1" __all__ = ["__version__"] diff --git a/packages/pb-client/CHANGELOG.md b/packages/pb-client/CHANGELOG.md index 634a710..db39a5b 100644 --- a/packages/pb-client/CHANGELOG.md +++ b/packages/pb-client/CHANGELOG.md @@ -9,6 +9,7 @@ Notable changes to `pybricks-client`. The format is based on - `connect()` gives an async handle to the service, starting one if needed. - Ongoing events are async context managers yielding async iterators. +- `watching_adapters()` says which Bluetooth adapters a search cannot use. - No function takes a `timeout`; callers scope cancellation themselves. - Compiles a script and every module of its own that it imports. - Passes a precompiled `.mpy` through, for modules built for native code. diff --git a/packages/pb-client/README.md b/packages/pb-client/README.md index 99e1c1e..343c89c 100644 --- a/packages/pb-client/README.md +++ b/packages/pb-client/README.md @@ -23,4 +23,4 @@ everything it opens is closed for you when the `async with` block ends. **Looking for the command line?** That is `pybricks-cli`, which installs `pb` and uses this library underneath. -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. diff --git a/packages/pb-client/pyproject.toml b/packages/pb-client/pyproject.toml index aac55b5..292135d 100644 --- a/packages/pb-client/pyproject.toml +++ b/packages/pb-client/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pybricks-client" -version = "0.1.0" +version = "1.0.0b1" description = "Client library for the Pybricks service" readme = "README.md" license = "MIT" @@ -10,6 +10,7 @@ maintainers = [{ name = "David Lechner", email = "david@pybricks.com" }] requires-python = ">=3.11" keywords = ["pybricks", "lego", "micropython", "bluetooth", "robotics"] classifiers = [ + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Education", "Natural Language :: English", @@ -18,7 +19,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "pybricks-core", + "pybricks-core>=1.0.0b1", "anyio>=4.14", "mpy-cross-v6>=1.1.1", "typing-extensions>=4.15", @@ -27,7 +28,7 @@ dependencies = [ [project.urls] homepage = "https://pybricks.com" repository = "https://github.com/pybricks/pybricks-tools" -documentation = "https://pybricks.github.io/pybricks-tools/reference/" +documentation = "https://pybricks.com/pybricks-tools/reference/" "Changelog" = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-client/CHANGELOG.md" "Support" = "https://github.com/pybricks/support/discussions" "Issues" = "https://github.com/pybricks/support/issues" diff --git a/packages/pb-client/src/pb_client/__init__.py b/packages/pb-client/src/pb_client/__init__.py index de87568..1a1a213 100644 --- a/packages/pb-client/src/pb_client/__init__.py +++ b/packages/pb-client/src/pb_client/__init__.py @@ -11,7 +11,7 @@ [move_on_after][anyio.move_on_after] instead. """ -__version__ = "0.1.0" +__version__ = "1.0.0b1" from pb_client.compile import ( SOURCE_NAME, diff --git a/packages/pb-core/README.md b/packages/pb-core/README.md index e6ce2d8..a0e5979 100644 --- a/packages/pb-core/README.md +++ b/packages/pb-core/README.md @@ -9,4 +9,4 @@ file locations all of them use. `pybricks-client`, which is the one to reach for if you want to drive a hub from your own Python. -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. diff --git a/packages/pb-core/pyproject.toml b/packages/pb-core/pyproject.toml index 5aadd9f..5493545 100644 --- a/packages/pb-core/pyproject.toml +++ b/packages/pb-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pybricks-core" -version = "0.1.0" +version = "1.0.0b1" description = "Shared protocol, IPC transport and logging for the Pybricks tools" readme = "README.md" license = "MIT" @@ -10,6 +10,7 @@ maintainers = [{ name = "David Lechner", email = "david@pybricks.com" }] requires-python = ">=3.11" keywords = ["pybricks", "lego", "micropython", "bluetooth", "robotics"] classifiers = [ + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Education", "Natural Language :: English", @@ -26,7 +27,7 @@ dependencies = [ [project.urls] homepage = "https://pybricks.com" repository = "https://github.com/pybricks/pybricks-tools" -documentation = "https://pybricks.github.io/pybricks-tools/design/" +documentation = "https://pybricks.com/pybricks-tools/design/" "Changelog" = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-core/CHANGELOG.md" "Support" = "https://github.com/pybricks/support/discussions" "Issues" = "https://github.com/pybricks/support/issues" diff --git a/packages/pb-core/src/pb_core/__init__.py b/packages/pb-core/src/pb_core/__init__.py index c0ed8df..674ec45 100644 --- a/packages/pb-core/src/pb_core/__init__.py +++ b/packages/pb-core/src/pb_core/__init__.py @@ -8,6 +8,6 @@ platform paths. It deliberately depends on neither ``pb_service`` nor ``pb_client``. """ -__version__ = "0.1.0" +__version__ = "1.0.0b1" __all__ = ["__version__"] diff --git a/packages/pb-core/tests/conftest.py b/packages/pb-core/tests/conftest.py index dac77e1..f0308db 100644 --- a/packages/pb-core/tests/conftest.py +++ b/packages/pb-core/tests/conftest.py @@ -1,9 +1,12 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2026 The Pybricks Authors +import shutil import socket import sys +import tempfile import uuid +from collections.abc import Iterator import pytest @@ -31,9 +34,7 @@ def _schemes() -> list[str]: @pytest.fixture(params=_schemes()) -def endpoint( - request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory -) -> Endpoint: +def endpoint(request: pytest.FixtureRequest) -> Iterator[Endpoint]: """A unique, unused endpoint for each transport under test. Uniqueness matters: named pipe names are machine-global, so a fixed name @@ -43,14 +44,25 @@ def endpoint( unique = uuid.uuid4().hex[:12] if scheme == "memory": - return Endpoint("memory", f"test-{unique}") + yield Endpoint("memory", f"test-{unique}") + return if scheme == "tcp": - return Endpoint("tcp", f"127.0.0.1:{_free_tcp_port()}") + yield Endpoint("tcp", f"127.0.0.1:{_free_tcp_port()}") + return if scheme == "pipe": - return Endpoint("pipe", f"pb-test-{unique}") - - # AF_UNIX: keep the path short, since sun_path is only 104 bytes on macOS. - directory = tmp_path_factory.mktemp("s") - return Endpoint("unix", str(directory / f"{unique}.sock")) + yield Endpoint("pipe", f"pb-test-{unique}") + return + + # AF_UNIX: under /tmp rather than pytest's tmp_path, since sun_path is only + # 104 bytes on macOS. The per-user temporary directory there is 57 bytes + # once resolved, and with pytest's own two levels a socket in it measures + # 104 as soon as pytest's run counter reaches three digits -- one too many, + # counting the terminator. tests/e2e/conftest.py's short_tmp_path does the + # same for the end-to-end tests. + directory = tempfile.mkdtemp(prefix="pb-", dir="/tmp") + try: + yield Endpoint("unix", f"{directory}/{unique}.sock") + finally: + shutil.rmtree(directory, ignore_errors=True) diff --git a/packages/pb-service/CHANGELOG.md b/packages/pb-service/CHANGELOG.md index e0edd3d..544f5b4 100644 --- a/packages/pb-service/CHANGELOG.md +++ b/packages/pb-service/CHANGELOG.md @@ -12,7 +12,11 @@ Notable changes to `pybricks-service`. The format is based on - Single-instance lock, shared-secret authentication, and a startup log line. - Exits when idle, but never while a hub is still connected. - Bluetooth through bleak, behind a seam that USB and RFCOMM can reuse. +- EV3 and NXT hubs over Bluetooth Classic, found among the paired devices. +- Says when macOS is asking for Bluetooth permission, and waits for the answer. +- A serial port that will not open is reported, not taken for a missing hub. - The Pybricks hub protocol, gated by profile version wherever it changed. +- Speaks every Pybricks Profile version up to v1.7.0. - Scanning that reports each hub as it is heard, with type and signal strength. - Remembers the hub chosen with `pb use`, in the user's state directory. - A simulated hub, so the whole service can be exercised without a radio. diff --git a/packages/pb-service/README.md b/packages/pb-service/README.md index c9b8604..9131a9a 100644 --- a/packages/pb-service/README.md +++ b/packages/pb-service/README.md @@ -10,4 +10,4 @@ session and a program watching the same hub do not get in each other's way. **You do not install this yourself.** It comes with `pybricks-cli`, and it starts when something needs it and stops when nothing has for a while. -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. diff --git a/packages/pb-service/pyproject.toml b/packages/pb-service/pyproject.toml index 44f3981..c94b0be 100644 --- a/packages/pb-service/pyproject.toml +++ b/packages/pb-service/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pybricks-service" -version = "0.1.0" +version = "1.0.0b1" description = "The Pybricks service: manages hub connections" readme = "README.md" license = "MIT" @@ -10,6 +10,7 @@ maintainers = [{ name = "David Lechner", email = "david@pybricks.com" }] requires-python = ">=3.11" keywords = ["pybricks", "lego", "micropython", "bluetooth", "robotics"] classifiers = [ + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Education", "Natural Language :: English", @@ -18,7 +19,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "pybricks-core", + "pybricks-core>=1.0.0b1", "anyio>=4.14", "bleak>=1.1.0", "packaging>=22", @@ -34,7 +35,7 @@ pb-service = "pb_service.cli:main" [project.urls] homepage = "https://pybricks.com" repository = "https://github.com/pybricks/pybricks-tools" -documentation = "https://pybricks.github.io/pybricks-tools/design/" +documentation = "https://pybricks.com/pybricks-tools/design/" "Changelog" = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-service/CHANGELOG.md" "Support" = "https://github.com/pybricks/support/discussions" "Issues" = "https://github.com/pybricks/support/issues" diff --git a/packages/pb-service/src/pb_service/__init__.py b/packages/pb-service/src/pb_service/__init__.py index 9e2a3d4..9d2b4b0 100644 --- a/packages/pb-service/src/pb_service/__init__.py +++ b/packages/pb-service/src/pb_service/__init__.py @@ -8,6 +8,6 @@ once. """ -__version__ = "0.1.0" +__version__ = "1.0.0b1" __all__ = ["__version__"] diff --git a/properdocs.yml b/properdocs.yml index cf44ead..ca78c1e 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -1,6 +1,6 @@ site_name: Pybricks Tools site_description: A service, library and CLI for talking to Pybricks hubs -site_url: https://pybricks.github.io/pybricks-tools/ +site_url: https://pybricks.com/pybricks-tools/ repo_url: https://github.com/pybricks/pybricks-tools edit_uri: edit/main/docs/ diff --git a/uv.lock b/uv.lock index 4c8d7c3..1ef0ede 100644 --- a/uv.lock +++ b/uv.lock @@ -893,7 +893,7 @@ wheels = [ [[package]] name = "pybricks-cli" -version = "0.1.0" +version = "1.0.0b1" source = { editable = "packages/pb-cli" } dependencies = [ { name = "anyio" }, @@ -920,7 +920,7 @@ requires-dist = [ [[package]] name = "pybricks-client" -version = "0.1.0" +version = "1.0.0b1" source = { editable = "packages/pb-client" } dependencies = [ { name = "anyio" }, @@ -939,7 +939,7 @@ requires-dist = [ [[package]] name = "pybricks-core" -version = "0.1.0" +version = "1.0.0b1" source = { editable = "packages/pb-core" } dependencies = [ { name = "anyio" }, @@ -956,7 +956,7 @@ requires-dist = [ [[package]] name = "pybricks-service" -version = "0.1.0" +version = "1.0.0b1" source = { editable = "packages/pb-service" } dependencies = [ { name = "aio-rfcomm" },