diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e83fb18 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,54 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build package + run: python -m build + + - name: Upload distribution artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + + environment: + name: pypi + url: https://pypi.org/p/vudials_client + + permissions: + id-token: write # required for OIDC trusted publishing + + steps: + - name: Download distribution artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..51f2424 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,149 @@ +# CLAUDE.md + +## Project Overview + +`vudials_client` is a Python client library for Streacom's VU1 Dial hardware. It exposes two main classes: + +- **`VUDial`** — controls individual dials (value, color, background image, name, easing) +- **`VUAdmin`** — manages the VU1 server (API key lifecycle, dial provisioning) + +The library communicates with a local VU1 server over plain HTTP using a key-in-URL query-parameter authentication scheme. + +**Package name (PyPI):** `vudials-client` +**Import path:** `from vudials_client import vudialsclient` +**Current version:** `2025.9.3` (calendar-versioned: `YYYY.M.patch`) +**Requires:** Python ≥ 3.11, `requests ≥ 2.32.5` + +--- + +## Repository Layout + +``` +src/vudials_client/ + __init__.py # empty — intentional + vudialsclient.py # VUUtil, VUAdminUtil, VUDial, VUAdmin + +tests/ + __init__.py + conftest.py # pytest fixtures: vudial, vuadmin + test_vudialsclient.py + +.github/workflows/ + ci.yml # matrix CI: Python 3.11 / 3.12 / 3.13 + +pyproject.toml # build system, deps, pytest & coverage config +pydoc-markdown.yml # generates docs/api.md from docstrings +requirements.txt # loose runtime dep (requests>=2.0.0) +``` + +--- + +## Development Setup + +```bash +git clone git@github.com:erinlkolp/vu1-dial-python-module.git +cd vu1-dial-python-module +pip install -e ".[dev]" +``` + +The `[dev]` extra installs: `pytest>=8.0`, `responses>=0.25`, `pytest-cov>=5.0`. + +--- + +## Running Tests + +```bash +# Run all tests with coverage +pytest tests/ -v --cov=vudials_client --cov-report=term-missing + +# Run a specific test class +pytest tests/test_vudialsclient.py::TestVUDialSetDialColor -v + +# Run without coverage (faster) +pytest tests/ -v +``` + +Tests use the `responses` library to mock all HTTP calls — no live server is needed. The two shared fixtures in `conftest.py` are `vudial` (a `VUDial` pointed at `localhost:5340`) and `vuadmin` (a `VUAdmin` pointed at the same address). + +--- + +## Class Architecture + +### Utility base classes (internal) + +| Class | Purpose | +|---|---| +| `VUUtil` | Builds `key=` query-param URIs and dispatches GET / multipart-POST | +| `VUAdminUtil` | Builds `admin_key=` query-param URIs and dispatches GET / POST | + +### Public classes + +| Class | Inherits | Key concern | +|---|---|---| +| `VUDial` | `VUUtil` | Dial control: value, color, image, name, easing | +| `VUAdmin` | `VUAdminUtil` | Server admin: provision dials, CRUD on API keys | + +Every method returns a raw `requests.Response`. Callers must parse `.json()` or check `.status_code` themselves. All methods call `raise_for_status()` internally, so HTTP 4xx/5xx raise `requests.exceptions.HTTPError`. + +--- + +## Security Notes + +These are pre-existing design constraints of the VU1 server API — do not silently "fix" them: + +1. **Plain HTTP only.** The server only listens on HTTP; HTTPS is not supported. Keep traffic on trusted local interfaces. +2. **Key-in-URL authentication.** Both `key=` and `admin_key=` parameters appear in the query string and will be recorded in server access logs, proxy logs, and HTTP client history. This is intentional until the upstream server adds header-based auth. +3. **No input validation in the library.** The library does not validate value ranges (e.g., RGB 0–255) or UID format; that is the server's responsibility. + +--- + +## Code Conventions + +- **No `logging.basicConfig()` in library code.** The module registers a named logger (`logging.getLogger(__name__)`) but never configures the root logger. Leave this pattern in place. +- **`urllib.parse.quote` for user-supplied strings** in URL path segments (`uid`) and most query parameters to prevent URL injection. Do not remove these calls. +- **Type annotations on all public methods.** Parameters and return types (`-> requests.Response`) must be annotated. +- **Docstrings on all public methods** using the `:param name: description` / `:return:` style already present. + +--- + +## Adding New API Endpoints + +1. Identify whether the new endpoint belongs to the dial API (use `VUDial` + `VUUtil`) or the admin API (use `VUAdmin` + `VUAdminUtil`). +2. URL-encode any user-supplied path segment with `quote(value, safe="")`. +3. URL-encode query-parameter values with `quote(value)`. +4. Coerce numeric parameters with `int()` before interpolating into the query string. +5. Return the raw `requests.Response` without unwrapping JSON. +6. Add a corresponding test class in `tests/test_vudialsclient.py` following the existing pattern: at minimum test the success path, the correct endpoint, key parameters, and HTTP error propagation. + +--- + +## Versioning & Publishing + +The project uses **calendar versioning** (`YYYY.M.patch`). To release a new version: + +1. Update `version` in `pyproject.toml`. +2. Ensure the changelog / README reflects the change. +3. Tag the commit and push; PyPI publishing is done manually via `hatch build && hatch publish` or the equivalent `twine upload`. + +--- + +## CI/CD + +GitHub Actions runs `.github/workflows/ci.yml` on every push to `main` or any `claude/**` branch, and on PRs targeting `main`. + +- Matrix: Python 3.11, 3.12, 3.13 on `ubuntu-latest` +- Command: `pytest tests/ -v --cov=vudials_client --cov-report=term-missing --cov-report=xml` +- Coverage XML is uploaded as a build artifact (Python 3.12 run only) + +--- + +## Documentation Generation + +API docs in `docs/api.md` are generated from docstrings via `pydoc-markdown`: + +```bash +pip install pydoc-markdown +pydoc-markdown +``` + +Configuration is in `pydoc-markdown.yml` (source: `src/vudials_client/`, output: `docs/api.md`). diff --git a/README.md b/README.md index 5471e2d..e2ca588 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ # vudials_client [![PyPI version](https://badge.fury.io/py/vudials_client.svg)](https://badge.fury.io/py/vudials_client) +[![CI](https://github.com/erinlkolp/vu1-dial-python-module/actions/workflows/ci.yml/badge.svg)](https://github.com/erinlkolp/vu1-dial-python-module/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) -A Python client module for Streacom's VU1 Dial development hardware. This provides a simple interface to manipulate multiple dials. +A Python client library for Streacom's VU1 Dial development hardware. Provides a simple, tested interface for controlling multiple dials and managing the VU1 server. ## Features -- **Full Dial API**: Easily change your dial's value, color, name, background image, and more! -- **Full Admin API**: Provides VU1 Dial Server API key management. +- **Full Dial API**: Change a dial's value, backlight color, name, background image, and easing parameters +- **Full Admin API**: VU1 Dial Server API key management and dial provisioning +- **Type-annotated**: All public methods carry full parameter and return-type annotations +- **Well tested**: Comprehensive test suite using `pytest` and `responses` — no live hardware required +- **CI tested**: Passes on Python 3.11, 3.12, and 3.13 via GitHub Actions ## Installation @@ -16,119 +21,143 @@ A Python client module for Streacom's VU1 Dial development hardware. This provid pip install vudials-client ``` -For development installation: +For development: ```bash git clone git@github.com:erinlkolp/vu1-dial-python-module.git -cd vu1-dial-python-module/ +cd vu1-dial-python-module pip install -e ".[dev]" ``` ## Quick Start -Here's a simple example to get you started: - ```python +import os from vudials_client import vudialsclient -server_key = os.environ['API_KEY'] -admin_key = os.environ['ADMIN_API_KEY'] -server_address = os.environ['VU1_SERVER_ADDRESS'] -server_port = os.environ['VU1_SERVER_PORT'] +server_address = os.environ["VU1_SERVER_ADDRESS"] +server_port = int(os.environ["VU1_SERVER_PORT"]) +server_key = os.environ["API_KEY"] +admin_key = os.environ["ADMIN_API_KEY"] vu_meter = vudialsclient.VUDial(server_address, server_port, server_key) -admin_api = vudialsclient.VUAdmin(server_address, server_port, admin_key) +admin_api = vudialsclient.VUAdmin(server_address, server_port, admin_key) +# List connected dials dial_list = vu_meter.list_dials() -print(dial_list) +print(dial_list.json()) + +# Set first dial to 75% with a blue backlight +uid = dial_list.json()[0]["uid"] +vu_meter.set_dial_value(uid, 75) +vu_meter.set_dial_color(uid, 0, 0, 255) +# List API keys api_key_list = admin_api.list_api_keys() -print(api_key_list) +print(api_key_list.json()) ``` -## Documentation - -For detailed documentation, see [the official documentation](https://github.com/erinlkolp/vu1-dial-python-module/blob/main/docs/api.md). +> **Note:** The VU1 server communicates over plain HTTP on your local network. Keep the server on a trusted interface and treat the API keys as secrets. Both keys are passed as URL query parameters and will appear in server access logs. -### Main Classes +## API Reference -#### `VUDial` +### `VUDial` -The primary class for interacting with the client-side module. +Controls dial hardware. All methods return a `requests.Response`; HTTP 4xx/5xx errors raise `requests.exceptions.HTTPError`. ```python -vu_meter = vudialsclient.VUDial(server_address, server_port, api_key) +vu_meter = vudialsclient.VUDial(server_address, server_port, api_key) ``` -**Parameters:** -- `server_address` (str): VU1 Dials Server host (ie. localhost) -- `server_port` (int): VU1 Dials Server port (ie. 5340) -- `api_key` (str): A valid API key for the VU1 Dials Server +| Parameter | Type | Description | +|---|---|---| +| `server_address` | `str` | VU1 server hostname or IP (e.g. `"localhost"`) | +| `server_port` | `int` | VU1 server port (e.g. `5340`) | +| `api_key` | `str` | A valid API key for the VU1 server | **Methods:** -- `list_dials()`: Processes the given data and returns a result -- `get_dial_info(uid)`: Saves the current state to a file -- `set_dial_value(uid, value)`: Sets a dial's value (position) -- `set_dial_color(uid, red, green, blue)`: Sets a dial's backlight color -- `set_dial_background(uid, file)`: Sets a dial's background image -- `get_dial_image_crc(uid)`: Obtains a dial's image CRC -- `set_dial_name(uid, name)`: Sets a dial's name (no spaces) -- `reload_hw_info(uid)`: Reloads dial hardware information -- `set_dial_easing(uid, period, step)`: Sets dial easing -- `set_backlight_easing(uid, period, step)`: Sets dial easing -- `get_easing_config(uid)`: Gets easing config for dial (unsupported as of now) - -#### `VUAdmin` - -The primary class for interacting with the client-side module. + +| Method | Description | +|---|---| +| `list_dials()` | List all connected dials | +| `get_dial_info(uid)` | Get status/info for a specific dial | +| `set_dial_value(uid, value)` | Set the dial position (0–100) | +| `set_dial_color(uid, red, green, blue)` | Set the backlight color (0–255 each channel) | +| `set_dial_background(uid, file)` | Upload a background image file | +| `get_dial_image_crc(uid)` | Get the CRC of the current background image | +| `set_dial_name(uid, name)` | Assign a name to a dial | +| `reload_hw_info(uid)` | Reload hardware information for a dial | +| `set_dial_easing(uid, period, step)` | Configure dial movement easing | +| `set_backlight_easing(uid, period, step)` | Configure backlight easing | +| `get_easing_config(uid)` | Retrieve current easing configuration | + +### `VUAdmin` + +Manages the VU1 server. All methods return a `requests.Response`; HTTP 4xx/5xx errors raise `requests.exceptions.HTTPError`. ```python -admin_api = vudialsclient.VUAdmin(server_address, server_port, admin_key) +admin_api = vudialsclient.VUAdmin(server_address, server_port, admin_key) ``` -**Parameters:** -- `server_address` (str): VU1 Dials Server host (ie. localhost) -- `server_port` (int): VU1 Dials Server port (ie. 5340) -- `admin_key` (str): A valid Admin API key for the VU1 Dials Server +| Parameter | Type | Description | +|---|---|---| +| `server_address` | `str` | VU1 server hostname or IP | +| `server_port` | `int` | VU1 server port | +| `admin_key` | `str` | A valid Admin API key for the VU1 server | **Methods:** -- `provision_dials()`: Provisions new dial hardware -- `list_api_keys()`: Lists all VU Server API keys -- `remove_api_key(target_key)`: Removes an API key -- `create_api_key(name, dials)`: Creates an API key (see value in return) -- `update_api_key(name, target_key, dials)`: Updates an API key -## Contributing +| Method | Description | +|---|---| +| `provision_dials()` | Provision newly connected dial hardware | +| `list_api_keys()` | List all configured API keys | +| `create_api_key(name, dials)` | Create a new API key; the generated key is in the response | +| `update_api_key(name, target_key, dials)` | Update an existing API key | +| `remove_api_key(target_key)` | Remove an API key | -Contributions are welcome! Please feel free to submit a Pull Request. +## Testing -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request +The test suite uses [`responses`](https://github.com/getsentry/responses) to mock all HTTP calls — no VU1 hardware or running server is required. -Please make sure to update tests as appropriate and follow the code style guide. +```bash +# Run all tests with coverage report +pytest tests/ -v --cov=vudials_client --cov-report=term-missing -## License +# Run a specific test class +pytest tests/test_vudialsclient.py::TestVUDialSetDialColor -v +``` + +CI runs automatically on every push and pull request via GitHub Actions across Python 3.11, 3.12, and 3.13. -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +## Documentation -## Acknowledgements +Full API documentation can be regenerated from docstrings: -- Many thanks to Aaron D. and Christopher K.! +```bash +pip install pydoc-markdown +pydoc-markdown +``` -## License & Author +This writes `docs/api.md` using the configuration in `pydoc-markdown.yml`. -- Author:: Erin L. Kolp () +## Contributing -Copyright (c) 2025 Erin L. Kolp +Contributions are welcome! Please open a pull request. + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/my-feature`) +3. Make your changes and add tests for any new or modified behaviour +4. Ensure the test suite passes: `pytest tests/ -v` +5. Open a pull request against `main` + +Please follow the existing code style: type-annotate all public methods, URL-encode user-supplied path segments with `quote(value, safe="")`, and keep library code free of `logging.basicConfig()` calls. + +## License -Licensed under the MIT License +MIT — see [LICENSE](LICENSE). -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to the following conditions: +## Author -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Erin L. Kolp () -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +Many thanks to Aaron D. and Christopher K.!