From 128e895d493d7287e4c2d4b7cd51edbe8fcac46f Mon Sep 17 00:00:00 2001 From: Geoff Bache Date: Tue, 26 May 2026 15:30:36 +0200 Subject: [PATCH 1/6] fixes for 3.14 --- CLAUDE.md | 58 +++++++++++++++++++++++++++++++++ examples/advanced_example.py | 17 +++++----- examples/asyncio_everything.py | 2 +- examples/basic_example.py | 9 +++-- examples/control_example.py | 3 +- examples/stream_6dof_example.py | 2 +- 6 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4683df9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project overview + +`qtm_rt` is the Qualisys SDK for Python — a client library that implements Qualisys' RealTime (RT) protocol for talking to QTM (Qualisys Track Manager). Published to PyPI as `qtm-rt`. Targets Python 3.5.3+ and RT protocol version 1.8+. Little-endian only; default port 22223. + +## Common commands + +```bash +# Setup (from README.md) +python -m venv .venv +source ./.venv/Scripts/activate # Windows bash; on PowerShell: .\.venv\Scripts\Activate.ps1 +pip install -r requirements-dev.txt + +# Tests +pytest test/ +pytest test/qrtconnection_test.py::test_connect_no_loop # single test + +# Build sdist + wheel into dist/ +python -m build + +# Build Sphinx docs into docs/_build/html/ +make -C docs html + +# Enable debug logging at runtime +QTM_LOGGING=debug python your_script.py +``` + +Release flow (from README) — bump `version` in `setup.py`, build, copy `docs/_build/html/*` into the sibling `../qualisys_python_sdk_gh_pages` checkout **preserving the legacy `v102/`, `v103/`, `v212/` directories**, commit/push that branch, `twine upload dist/*`, then `git tag vX.Y.Z && git push --tags` and create a GitHub release manually. + +## Architecture + +Everything is `asyncio`-based and built around `qtm_rt.connect()` → returns a `QRTConnection`. The data flow: + +- **`qrt.py`** — public API surface. `connect()` opens a TCP connection wrapped in `QTMProtocol`, negotiates the RT protocol version (default `"1.25"`), and returns a `QRTConnection` exposing async methods for every RT command (`stream_frames`, `get_current_frame`, `get_parameters`, `take_control`, `start`, `stop`, `load`, `save`, `calibrate`, etc.). The `@validate_response([...])` decorator asserts the server's reply starts with an expected prefix and raises `QRTCommandException` otherwise. +- **`protocol.py`** — `QTMProtocol` is an `asyncio.Protocol` subclass. Outgoing commands are framed with `RTheader` (` Date: Tue, 26 May 2026 16:22:29 +0200 Subject: [PATCH 2/6] Make 3.7 explicit as the earliest supported version --- CLAUDE.md | 3 +-- docs/index.rst | 2 +- qtm_rt/__init__.py | 15 +++++---------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4683df9..4f42f9b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project overview -`qtm_rt` is the Qualisys SDK for Python — a client library that implements Qualisys' RealTime (RT) protocol for talking to QTM (Qualisys Track Manager). Published to PyPI as `qtm-rt`. Targets Python 3.5.3+ and RT protocol version 1.8+. Little-endian only; default port 22223. +`qtm_rt` is the Qualisys SDK for Python — a client library that implements Qualisys' RealTime (RT) protocol for talking to QTM (Qualisys Track Manager). Published to PyPI as `qtm-rt`. Targets Python 3.7+ and RT protocol version 1.8+. Little-endian only; default port 22223. ## Common commands @@ -47,7 +47,6 @@ Key invariants when modifying: - Component and parameter names accepted by `stream_frames` / `get_current_frame` / `get_parameters` are validated against hardcoded allow-lists in `qrt.py` (`_validate_components` and the inline list in `get_parameters`). Adding a new RT component requires updating **both** that list and `packet.py`. - `request_queue` is consumed with `.pop()` (LIFO from the right), so its order must match the order replies arrive — be careful when adding new code paths that enqueue futures. - `on_packet` callbacks are sync; they run inside the asyncio event loop, so blocking work there will stall the protocol. -- The package's `__init__.py` gates the Python 3 imports behind a `PYTHON3` check — Python 2 callers only get `QRTPacket`, `QRTEvent`, `Receiver`. Don't move the Py3-only imports out of that guard unless dropping 3.5 support. ## Testing notes diff --git a/docs/index.rst b/docs/index.rst index 795099c..54df897 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,7 +18,7 @@ This document describes the Qualisys SDK for Python version 3.0.2 Installation: ------------- -This package is a pure python package and requires at least Python 3.5.3, the easiest way to install it is: +This package is a pure python package and requires at least Python 3.7, the easiest way to install it is: .. code-block:: console diff --git a/qtm_rt/__init__.py b/qtm_rt/__init__.py index e934db9..7ca1979 100644 --- a/qtm_rt/__init__.py +++ b/qtm_rt/__init__.py @@ -1,18 +1,13 @@ """ Python SDK for QTM """ import logging -import sys import os -PYTHON3 = sys.version_info.major == 3 - -if PYTHON3: - from .discovery import Discover - from .reboot import reboot - from .qrt import connect, QRTConnection - from .protocol import QRTCommandException - from .control import TakeControl - +from .discovery import Discover +from .reboot import reboot +from .qrt import connect, QRTConnection +from .protocol import QRTCommandException +from .control import TakeControl from .packet import QRTPacket, QRTEvent from .receiver import Receiver From bae7db17fcb3372d988c492c357af25cbda9be69 Mon Sep 17 00:00:00 2001 From: Geoff Bache Date: Tue, 26 May 2026 16:30:16 +0200 Subject: [PATCH 3/6] Fixing more removed modules --- examples/asyncio_everything.py | 4 ++-- examples/stream_6dof_example.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/asyncio_everything.py b/examples/asyncio_everything.py index 9b04a87..18e8bc1 100644 --- a/examples/asyncio_everything.py +++ b/examples/asyncio_everything.py @@ -3,7 +3,7 @@ import logging import asyncio import argparse -import pkg_resources +import os import qtm_rt @@ -12,7 +12,7 @@ LOG = logging.getLogger("example") -QTM_FILE = pkg_resources.resource_filename("qtm_rt", "data/Demo.qtm") +QTM_FILE = os.path.join(os.path.dirname(qtm_rt.__file__), "data", "Demo.qtm") class AsyncEnumerate: diff --git a/examples/stream_6dof_example.py b/examples/stream_6dof_example.py index 2994b02..8782864 100644 --- a/examples/stream_6dof_example.py +++ b/examples/stream_6dof_example.py @@ -3,12 +3,12 @@ """ import asyncio +import os import xml.etree.ElementTree as ET -import pkg_resources import qtm_rt -QTM_FILE = pkg_resources.resource_filename("qtm_rt", "data/Demo.qtm") +QTM_FILE = os.path.join(os.path.dirname(qtm_rt.__file__), "data", "Demo.qtm") def create_body_index(xml_string): From f263866b44bb0c3657299354309495e41f11e0a2 Mon Sep 17 00:00:00 2001 From: Martin Holmberg Date: Wed, 10 Jun 2026 21:05:14 +0200 Subject: [PATCH 4/6] Move Demo.qtm out of the package, attach to releases The file is no longer shipped in the wheel/sdist. Examples reference it via a path next to the script, and the release workflow attaches it to the GitHub Release as a downloadable asset. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/release.yml | 7 +++++++ README.md | 5 ++++- {qtm_rt/data => examples}/Demo.qtm | Bin examples/asyncio_everything.py | 2 +- examples/stream_6dof_example.py | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) rename {qtm_rt/data => examples}/Demo.qtm (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a7e7138..9464c3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,8 @@ jobs: build: name: Build and test runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Check out repository @@ -38,6 +40,11 @@ jobs: name: distributions path: dist/ + - name: Attach Demo.qtm to GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "${{ github.event.release.tag_name }}" examples/Demo.qtm --repo "${{ github.repository }}" --clobber + publish: name: Publish to PyPI runs-on: ubuntu-latest diff --git a/README.md b/README.md index 51a86df..22903f5 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ https://qualisys.github.io/qualisys_python_sdk/index.html Examples -------- -See the examples folder. +See the examples folder. Some examples load a `Demo.qtm` recording — it ships +alongside the example scripts in this repo, and is also attached to each +[GitHub Release](https://github.com/qualisys/qualisys_python_sdk/releases) +for users who installed via pip. Logging ------- diff --git a/qtm_rt/data/Demo.qtm b/examples/Demo.qtm similarity index 100% rename from qtm_rt/data/Demo.qtm rename to examples/Demo.qtm diff --git a/examples/asyncio_everything.py b/examples/asyncio_everything.py index 18e8bc1..46308b7 100644 --- a/examples/asyncio_everything.py +++ b/examples/asyncio_everything.py @@ -12,7 +12,7 @@ LOG = logging.getLogger("example") -QTM_FILE = os.path.join(os.path.dirname(qtm_rt.__file__), "data", "Demo.qtm") +QTM_FILE = os.path.join(os.path.dirname(__file__), "Demo.qtm") class AsyncEnumerate: diff --git a/examples/stream_6dof_example.py b/examples/stream_6dof_example.py index 8782864..eebb3a6 100644 --- a/examples/stream_6dof_example.py +++ b/examples/stream_6dof_example.py @@ -8,7 +8,7 @@ import qtm_rt -QTM_FILE = os.path.join(os.path.dirname(qtm_rt.__file__), "data", "Demo.qtm") +QTM_FILE = os.path.join(os.path.dirname(__file__), "Demo.qtm") def create_body_index(xml_string): From feb6fe06de0909d748702850acc9bb5db4a97f30 Mon Sep 17 00:00:00 2001 From: Martin Holmberg Date: Wed, 10 Jun 2026 22:56:07 +0200 Subject: [PATCH 5/6] Make QRTConnection an async context manager Closes the asyncio transport-leak ResourceWarnings that surface under Python 3.14 (the GC-based cleanup path no longer fires before the warning). QRTConnection now supports `async with` so the transport is closed deterministically on scope exit; disconnect() is idempotent. Discover does the same for its UDP datagram transport. All seven examples updated to use the new context-manager form. Verified end-to-end against QTM 2026.3 Beta on Python 3.14: - basic, stream_6dof, advanced, control: full pass, no ResourceWarning. - image, calibration, asyncio_everything: fail at QTM-environmental preconditions (camera image mode, live capture for calibration, trigger configuration) but shut down cleanly, no ResourceWarning. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/advanced_example.py | 2 +- examples/asyncio_everything.py | 89 ++++++++++++++++----------------- examples/basic_example.py | 9 ++-- examples/calibration_example.py | 6 +-- examples/control_example.py | 4 +- examples/image_example.py | 29 ++++++----- examples/stream_6dof_example.py | 83 +++++++++++++++--------------- qtm_rt/discovery.py | 9 +++- qtm_rt/qrt.py | 11 +++- 9 files changed, 127 insertions(+), 115 deletions(-) diff --git a/examples/advanced_example.py b/examples/advanced_example.py index b504da7..c69b9ba 100644 --- a/examples/advanced_example.py +++ b/examples/advanced_example.py @@ -53,7 +53,7 @@ async def main(): if connection is None: return -1 - async with qtm_rt.TakeControl(connection, "password"): + async with connection, qtm_rt.TakeControl(connection, "password"): state = await connection.get_state() if state != qtm_rt.QRTEvent.EventConnected: diff --git a/examples/asyncio_everything.py b/examples/asyncio_everything.py index 46308b7..26cc8f1 100644 --- a/examples/asyncio_everything.py +++ b/examples/asyncio_everything.py @@ -80,74 +80,73 @@ async def main(interface=None): if connection is None: return - await connection.get_state() - await connection.byte_order() + async with connection: + await connection.get_state() + await connection.byte_order() + + async with qtm_rt.TakeControl(connection, "password"): - async with qtm_rt.TakeControl(connection, "password"): + result = await connection.close() + if result == b"Closing connection": + await connection.await_event(qtm_rt.QRTEvent.EventConnectionClosed) - result = await connection.close() - if result == b"Closing connection": - await connection.await_event(qtm_rt.QRTEvent.EventConnectionClosed) + await connection.load(QTM_FILE) - await connection.load(QTM_FILE) + await connection.start(rtfromfile=True) - await connection.start(rtfromfile=True) + (await connection.get_current_frame(components=["3d"])).get_3d_markers() - (await connection.get_current_frame(components=["3d"])).get_3d_markers() + queue = asyncio.Queue() - queue = asyncio.Queue() + asyncio.ensure_future(packet_receiver(queue)) - asyncio.ensure_future(packet_receiver(queue)) + try: + await connection.stream_frames( + components=["incorrect"], on_packet=queue.put_nowait + ) + except qtm_rt.QRTCommandException as exception: + LOG.info("exception %s", exception) - try: await connection.stream_frames( - components=["incorrect"], on_packet=queue.put_nowait + components=["3d"], on_packet=queue.put_nowait ) - except qtm_rt.QRTCommandException as exception: - LOG.info("exception %s", exception) - - await connection.stream_frames( - components=["3d"], on_packet=queue.put_nowait - ) - - await asyncio.sleep(0.5) - await connection.byte_order() - await asyncio.sleep(0.5) - await connection.stream_frames_stop() - queue.put_nowait(None) - await connection.get_parameters(parameters=["3d"]) - await connection.stop() + await asyncio.sleep(0.5) + await connection.byte_order() + await asyncio.sleep(0.5) + await connection.stream_frames_stop() + queue.put_nowait(None) - await connection.await_event() + await connection.get_parameters(parameters=["3d"]) + await connection.stop() - await connection.new() - await connection.await_event(qtm_rt.QRTEvent.EventConnected) + await connection.await_event() - await connection.start() - await connection.await_event(qtm_rt.QRTEvent.EventWaitingForTrigger) + await connection.new() + await connection.await_event(qtm_rt.QRTEvent.EventConnected) - await connection.trig() - await connection.await_event(qtm_rt.QRTEvent.EventCaptureStarted) + await connection.start() + await connection.await_event(qtm_rt.QRTEvent.EventWaitingForTrigger) - await asyncio.sleep(0.5) + await connection.trig() + await connection.await_event(qtm_rt.QRTEvent.EventCaptureStarted) - await connection.set_qtm_event() - await asyncio.sleep(0.001) - await connection.set_qtm_event("with_label") + await asyncio.sleep(0.5) - await asyncio.sleep(0.5) + await connection.set_qtm_event() + await asyncio.sleep(0.001) + await connection.set_qtm_event("with_label") - await connection.stop() - await connection.await_event(qtm_rt.QRTEvent.EventCaptureStopped) + await asyncio.sleep(0.5) - await connection.save(r"measurement.qtm") + await connection.stop() + await connection.await_event(qtm_rt.QRTEvent.EventCaptureStopped) - await asyncio.sleep(3) + await connection.save(r"measurement.qtm") - await connection.close() + await asyncio.sleep(3) - connection.disconnect() + await connection.close() def parse_args(): diff --git a/examples/basic_example.py b/examples/basic_example.py index fe4b9ea..708daa9 100644 --- a/examples/basic_example.py +++ b/examples/basic_example.py @@ -23,11 +23,12 @@ async def main(): if connection is None: return - await connection.stream_frames(components=["3d"], on_packet=on_packet) + async with connection: + await connection.stream_frames(components=["3d"], on_packet=on_packet) - # stream_frames returns immediately after registering the callback; keep the - # loop alive so packets keep arriving. Ctrl-C to stop. - await asyncio.Event().wait() + # stream_frames returns immediately after registering the callback; keep the + # loop alive so packets keep arriving. Ctrl-C to stop. + await asyncio.Event().wait() if __name__ == "__main__": diff --git a/examples/calibration_example.py b/examples/calibration_example.py index 17e3c98..60bc6d4 100644 --- a/examples/calibration_example.py +++ b/examples/calibration_example.py @@ -16,7 +16,7 @@ async def setup(): if connection is None: return -1 - async with qtm_rt.TakeControl(connection, "password"): + async with connection, qtm_rt.TakeControl(connection, "password"): state = await connection.get_state() if state != qtm_rt.QRTEvent.EventConnected: @@ -37,8 +37,8 @@ async def setup(): root = ET.fromstring(cal_response) print(ET.tostring(root, pretty_print=True).decode()) - # tell qtm to stop streaming - await connection.stream_frames_stop() + # tell qtm to stop streaming + await connection.stream_frames_stop() if __name__ == "__main__": asyncio.run(setup()) diff --git a/examples/control_example.py b/examples/control_example.py index d74f410..a05d2df 100644 --- a/examples/control_example.py +++ b/examples/control_example.py @@ -23,7 +23,7 @@ async def setup(): if connection is None: return -1 - async with qtm_rt.TakeControl(connection, "password"): + async with connection, qtm_rt.TakeControl(connection, "password"): state = await connection.get_state() if state != qtm_rt.QRTEvent.EventConnected: await connection.new() @@ -49,7 +49,5 @@ async def setup(): LOG.info("Measurement saved to Demo.qtm") - connection.disconnect() - if __name__ == "__main__": asyncio.run(setup()) diff --git a/examples/image_example.py b/examples/image_example.py index f73901f..bdd27a2 100644 --- a/examples/image_example.py +++ b/examples/image_example.py @@ -44,21 +44,20 @@ async def main(password, target_camera_id, output_path): if connection is None: raise RuntimeError("Failed to connect") - settings = await connection.get_parameters(parameters=["image"]) - updated_settings = enable_disable_cameras(output_path, target_camera_id, settings) - - async with qtm_rt.TakeControl(connection, password): - logging.debug("%s", await connection.send_xml(updated_settings)) - - frame = await connection.get_current_frame(components=["image"]) - info, images = frame.get_image() - logging.info("%s", info) - logging.info("%s", images[0][0]) - with open(output_path, "wb") as f: - logging.info("Writing %s", output_path) - f.write(images[0][1]) - - connection.disconnect() + async with connection: + settings = await connection.get_parameters(parameters=["image"]) + updated_settings = enable_disable_cameras(output_path, target_camera_id, settings) + + async with qtm_rt.TakeControl(connection, password): + logging.debug("%s", await connection.send_xml(updated_settings)) + + frame = await connection.get_current_frame(components=["image"]) + info, images = frame.get_image() + logging.info("%s", info) + logging.info("%s", images[0][0]) + with open(output_path, "wb") as f: + logging.info("Writing %s", output_path) + f.write(images[0][1]) if __name__ == "__main__": diff --git a/examples/stream_6dof_example.py b/examples/stream_6dof_example.py index eebb3a6..0d7cc90 100644 --- a/examples/stream_6dof_example.py +++ b/examples/stream_6dof_example.py @@ -35,55 +35,56 @@ async def main(): print("Failed to connect") return - # Take control of qtm, context manager will automatically release control after scope end - async with qtm_rt.TakeControl(connection, "password"): + async with connection: + # Take control of qtm, context manager will automatically release control after scope end + async with qtm_rt.TakeControl(connection, "password"): - realtime = False + realtime = False - if realtime: - # Start new realtime - await connection.new() - else: - # Load qtm file - await connection.load(QTM_FILE) + if realtime: + # Start new realtime + await connection.new() + else: + # Load qtm file + await connection.load(QTM_FILE) - # start rtfromfile - await connection.start(rtfromfile=True) + # start rtfromfile + await connection.start(rtfromfile=True) - # Get 6dof settings from qtm - xml_string = await connection.get_parameters(parameters=["6d"]) - body_index = create_body_index(xml_string) + # Get 6dof settings from qtm + xml_string = await connection.get_parameters(parameters=["6d"]) + body_index = create_body_index(xml_string) - print("{} of {} 6DoF bodies enabled".format(body_enabled_count(xml_string), len(body_index))) + print("{} of {} 6DoF bodies enabled".format(body_enabled_count(xml_string), len(body_index))) - wanted_body = "L-frame" + wanted_body = "L-frame" - def on_packet(packet): - info, bodies = packet.get_6d() - print( - "Framenumber: {} - Body count: {}".format( - packet.framenumber, info.body_count + def on_packet(packet): + info, bodies = packet.get_6d() + print( + "Framenumber: {} - Body count: {}".format( + packet.framenumber, info.body_count + ) ) - ) - - if wanted_body is not None and wanted_body in body_index: - # Extract one specific body - wanted_index = body_index[wanted_body] - position, rotation = bodies[wanted_index] - print("{} - Pos: {} - Rot: {}".format(wanted_body, position, rotation)) - else: - # Print all bodies - for position, rotation in bodies: - print("Pos: {} - Rot: {}".format(position, rotation)) - - # Start streaming frames - await connection.stream_frames(components=["6d"], on_packet=on_packet) - - # Wait asynchronously 5 seconds - await asyncio.sleep(5) - - # Stop streaming - await connection.stream_frames_stop() + + if wanted_body is not None and wanted_body in body_index: + # Extract one specific body + wanted_index = body_index[wanted_body] + position, rotation = bodies[wanted_index] + print("{} - Pos: {} - Rot: {}".format(wanted_body, position, rotation)) + else: + # Print all bodies + for position, rotation in bodies: + print("Pos: {} - Rot: {}".format(position, rotation)) + + # Start streaming frames + await connection.stream_frames(components=["6d"], on_packet=on_packet) + + # Wait asynchronously 5 seconds + await asyncio.sleep(5) + + # Stop streaming + await connection.stream_frames_stop() if __name__ == "__main__": diff --git a/qtm_rt/discovery.py b/qtm_rt/discovery.py index 936e170..2e9f52c 100644 --- a/qtm_rt/discovery.py +++ b/qtm_rt/discovery.py @@ -66,6 +66,7 @@ def __init__(self, ip_address): self.ip_address = ip_address self.queue = asyncio.Queue() self.first = True + self._transport = None def __aiter__(self): return self @@ -79,7 +80,7 @@ async def __anext__(self) -> QRTDiscoveryResponse: receiver=self.queue.put_nowait ) - _, protocol = await loop.create_datagram_endpoint( + self._transport, protocol = await loop.create_datagram_endpoint( protocol_factory, local_addr=(self.ip_address, 0), allow_broadcast=True, @@ -93,10 +94,16 @@ async def __anext__(self) -> QRTDiscoveryResponse: result = await self.queue.get() if result is None: LOG.debug("Discovery timed out") + self._close() raise StopAsyncIteration LOG.debug(result) call_handle.cancel() return result + def _close(self): + if self._transport is not None: + self._transport.close() + self._transport = None + diff --git a/qtm_rt/qrt.py b/qtm_rt/qrt.py index ff4b014..b7043bc 100644 --- a/qtm_rt/qrt.py +++ b/qtm_rt/qrt.py @@ -45,9 +45,16 @@ def __init__(self, protocol: QTMProtocol, timeout): self._protocol = protocol self._timeout = timeout + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + self.disconnect() + def disconnect(self): - """Disconnect from QTM.""" - self._protocol.transport.close() + """Disconnect from QTM. Safe to call multiple times.""" + if self._protocol.transport is not None: + self._protocol.transport.close() def has_transport(self): """ Check if connected to QTM """ From cdc3b211473cb1f801477c7afb11de80cb575155 Mon Sep 17 00:00:00 2001 From: Martin Holmberg Date: Wed, 10 Jun 2026 22:56:07 +0200 Subject: [PATCH 6/6] Update CLAUDE.md to current state - Python floor 3.7 -> 3.10 (per #50) - Release flow now goes through GitHub Actions / Trusted Publishing, pyproject.toml owns the version, setup.py is gone - request_queue is FIFO (popleft) after #48 - Mention QRTConnection's new async-context-manager surface Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4f42f9b..8a6cca3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project overview -`qtm_rt` is the Qualisys SDK for Python — a client library that implements Qualisys' RealTime (RT) protocol for talking to QTM (Qualisys Track Manager). Published to PyPI as `qtm-rt`. Targets Python 3.7+ and RT protocol version 1.8+. Little-endian only; default port 22223. +`qtm_rt` is the Qualisys SDK for Python — a client library that implements Qualisys' RealTime (RT) protocol for talking to QTM (Qualisys Track Manager). Published to PyPI as `qtm-rt`. Targets Python 3.10+ and RT protocol version 1.8+. Little-endian only; default port 22223. ## Common commands @@ -28,14 +28,14 @@ make -C docs html QTM_LOGGING=debug python your_script.py ``` -Release flow (from README) — bump `version` in `setup.py`, build, copy `docs/_build/html/*` into the sibling `../qualisys_python_sdk_gh_pages` checkout **preserving the legacy `v102/`, `v103/`, `v212/` directories**, commit/push that branch, `twine upload dist/*`, then `git tag vX.Y.Z && git push --tags` and create a GitHub release manually. +Release flow — bump `version` in `pyproject.toml` (and `docs/conf.py`), open a PR, merge to master, then create a GitHub Release on the tag `vX.Y.Z`. Publishing the release fires `.github/workflows/release.yml`, which runs tests, builds the sdist+wheel, twine-checks, uploads to PyPI via Trusted Publishing, and attaches `examples/Demo.qtm` to the release. Docs are still a manual step: build with `make -C docs html`, copy `docs/_build/html/*` into the sibling `../qualisys_python_sdk_gh_pages` checkout **preserving the legacy `v102/`, `v103/`, `v212/` directories**, commit/push that branch. ## Architecture Everything is `asyncio`-based and built around `qtm_rt.connect()` → returns a `QRTConnection`. The data flow: - **`qrt.py`** — public API surface. `connect()` opens a TCP connection wrapped in `QTMProtocol`, negotiates the RT protocol version (default `"1.25"`), and returns a `QRTConnection` exposing async methods for every RT command (`stream_frames`, `get_current_frame`, `get_parameters`, `take_control`, `start`, `stop`, `load`, `save`, `calibrate`, etc.). The `@validate_response([...])` decorator asserts the server's reply starts with an expected prefix and raises `QRTCommandException` otherwise. -- **`protocol.py`** — `QTMProtocol` is an `asyncio.Protocol` subclass. Outgoing commands are framed with `RTheader` (`