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
18 changes: 0 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,6 @@ jobs:
- run: ruff format --check .
- run: python -m pytest -q

homeassistant:
name: Home Assistant integration lint + test
runs-on: ubuntu-latest
defaults:
run:
working-directory: python/examples/homeassistant
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- uses: astral-sh/setup-uv@v5
- run: uv sync
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run pytest -q
- run: uv run python build.py

swift:
name: Swift lint + test
runs-on: macos-15
Expand Down
56 changes: 56 additions & 0 deletions .github/workflows/publish-python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Publish libkp to PyPI

# Tag-driven: `git tag python-v0.1.0 && git push --tags` builds python/ and
# publishes it. Authentication is PyPI Trusted Publishing over OIDC — the
# `id-token: write` permission below is the whole credential, so there is no
# API token in this repository.
on:
push:
tags: ["python-v*"]
workflow_dispatch:

permissions: {}

jobs:
build:
name: Build the distributions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"

- name: The tag and the package agree on the version
if: startsWith(github.ref, 'refs/tags/')
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME#python-v}"
pkg=$(grep -m1 '^version = ' python/pyproject.toml | cut -d'"' -f2)
init=$(grep -m1 '^__version__ = ' python/src/libkp/__init__.py | cut -d'"' -f2)
echo "tag=$tag pyproject=$pkg __version__=$init"
test "$tag" = "$pkg"
test "$tag" = "$init"

- run: pip install build
- run: python -m build python/
- uses: actions/upload-artifact@v4
with:
name: dist
path: python/dist/

publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/libkp
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ with my hands.
| Python | `libkp` | Python 3.11+, standard library only | [`python/`](python/) |
| Swift | `LibKP` | Swift 6, macOS 13+, `Network` framework | [`swift/`](swift/) |

None of the three is on a package registry yet; depend on the directory as a
path or git dependency. The shape of the API is the same everywhere — find a
device, connect a model, subscribe to its state, send it things:
Python is on PyPI — `pip install libkp`. Rust and Swift are not on a registry
yet; depend on the directory as a path or git dependency. The shape of the API
is the same everywhere — find a device, connect a model, subscribe to its
state, send it things:

```rust
use libkp::model::DeviceModel;
Expand Down Expand Up @@ -69,6 +70,14 @@ cd swift && swift run meters # or `swift run MetersApp` fo
Discovery needs UDP port 5727 to itself, so quit Kemper's Rig Manager (or pass
`--ip`) before running it.

### Built on libkp

- [**kemper-homeassistant**](https://github.com/gotwalt/kemper-homeassistant) —
a Home Assistant integration: the rig, the amp, the cabinet, and whether
anyone is playing, held on one session that never polls the device. It
depends on `libkp` from PyPI, and tests against
[`libkp.testing.FakeDevice`](python/README.md#testing-against-a-fake-profiler).

## Status

Tested against a **Profiler Player on firmware 14.2.1**. Other Profiler models
Expand Down
Binary file added assets/kemper.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 28 additions & 2 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ the CC control vocabulary, and an observable async device model.
## Install

```sh
pip install -e '.[dev]' # from python/
pip install libkp # from PyPI
pip install -e '.[dev]' # or from python/, to work on it
```

Or run straight from the source tree:
Expand Down Expand Up @@ -153,6 +154,7 @@ with DiscoveryPort.acquire() as port: # raises PortUnavailableError if taken
| `libkp.state` | The state tree and the pure `DeviceState.apply_update` fold. |
| `libkp.model` | `DeviceModel`, the async store over the stream and the control link. |
| `libkp.errors` | The exception family, all deriving from `LibKPError`. |
| `libkp.testing` | `FakeDevice`, an in-process Profiler to test against. |
| `libkp._generated` | **Generated, data only** — constants and lookup tables. Do not edit. |

`_generated.py` is emitted from [`../spec`](../spec) by
Expand Down Expand Up @@ -371,6 +373,30 @@ for message in unframer.push(raw_stream_bytes):
...
```

## Testing against a fake Profiler

`libkp.testing.FakeDevice` is a Profiler stand-in that speaks the real
transport in-process: the greeting, the protocol-selection handshake, the
preamble, then MIDI3 framing or the CBOR dump. Anything built on libkp can hold
a session against it in its own suite, with nothing below the socket mocked and
no device on the desk.

```python
from libkp import DeviceModel
from libkp.testing import FakeDevice, answer_requests

fake = await FakeDevice(responder=answer_requests).start()
model = await DeviceModel.connect("127.0.0.1", port=fake.port)
...
await model.close()
await fake.stop()
```

It can also hang up mid-session, hold back the greeting, or refuse connections
for a while — the states a reconnect has to survive. libkp's own async tests
drive it; so does the [Home Assistant
integration](https://github.com/gotwalt/kemper-homeassistant).

## Tests

```sh
Expand All @@ -388,7 +414,7 @@ The suite covers:
checked for message count, pending bytes, exact messages, decoded status
frames, the per-function histogram, and the resulting rig/amp/cab names.
- **Unit tests** for each module, and async tests that drive `Session` and
`DeviceModel` against an in-process stand-in device (`tests/fake_device.py`).
`DeviceModel` against an in-process stand-in device (`libkp.testing`, shipped with the package).

## Provenance

Expand Down
119 changes: 0 additions & 119 deletions python/examples/homeassistant/README.md

This file was deleted.

96 changes: 0 additions & 96 deletions python/examples/homeassistant/build.py

This file was deleted.

Loading
Loading