Repository navigation
Releases: dathere/qsv-client
Release list
0.4.0
qsv 25.0.0 is the first qsv release whose binaries (qsv, qsvlite, qsvdp, qsvmcp) all answer --capabilities and report errors as JSON. qsv-client 0.4.0 makes it the minimum and drops the code that supported older qsv.
⚠️ Behavior change: qsv 25.0.0 or later is required
Before its first command, every client now runs qsv --capabilities. Previously this check only happened when you passed min_version=. A qsv that is too old raises QsvVersionError instead of running:
qsv = Qsv("/usr/local/bin/qsv") # qsv 24.0.0
qsv.count("data.csv")
# QsvVersionError: /usr/local/bin/qsv does not support --capabilities;
# qsv 25.0.0 or later is requiredMIN_QSV_VERSION("25.0.0") is exported.min_version=can still raise the floor, but not lower it.- The check uses the run's
timeout(at most 60s), separately from the command. So a client's first run can take up to about twice its timeout. Later runs aren't affected. - If the check fails for another reason, such as a crash or a startup error, you get the usual
QsvErrorwith qsv's stderr, not a version error.
Upgrade qsv before upgrading qsv-client. (#9)
⚠️ Behavior change: QsvVersionError is now a QsvError
Because any run() can now raise it, QsvVersionError subclasses QsvError, with kind version. So except QsvError around run() catches a too-old binary too.
try:
qsv.run("stats", "data.csv")
except QsvVersionError: # must come first: a later `except QsvVersionError` is never reached
...
except QsvError:
...Generic QsvError handlers now see version errors as well. Retry logic shouldn't retry them, because a too-old qsv will never succeed. The error's exit_code, args_run and stderr describe the --capabilities check. (#10)
Capabilities are checked once per process
--capabilities now runs at most once per qsv binary per Python process, and every Qsv and AsyncQsv shares the result. So creating a client per call (for example, one per web request) doesn't start an extra qsv each time.
- Concurrent first calls from threads, or from coroutines on the same event loop, share one check.
- A binary replaced in place (new size or modification time) is checked again.
- Each client still applies its own
min_versionand gets its own copy of the result. - Values in
rawthat depend on the environment, such asmax_jobs, come from the client that checked first.
Removed
- The
--version/--listfallback, includingqsv_client.capabilities.from_version_output. If--capabilitiessucceeds but its output can't be used,capabilitiesraisesQsvError. (#9) Capabilities.supports_json_errorsandCapabilities.error_formats. Every supported qsv reports JSON errors. The value is still inraw["error_formats"]. (#10)Capabilities.rawis no longer optional. It's always the parsed--capabilitiesoutput, andCapabilitiesfields are now keyword-only. (#10)
Under the hood
- Errors are still classified by exit code when qsv dies without writing a JSON error line, for example after a panic or a signal.
- CI no longer runs the integration tests against a pinned qsv 24.0.0. Unit tests simulate a qsv that rejects
--capabilities, and the integration tests check that every binary variant reports JSON errors.
Full changelog: 0.3.0...0.4.0
0.3.0
⚠️ Behavior change: relative stdout_path now follows cwd=
A relative stdout_path is now resolved against the client's cwd=, the same way a relative --output path given to qsv is. Previously it was resolved against Python's working directory.
qsv = Qsv(cwd="/data")
qsv.run("stats", "in.csv", stdout_path="out.csv")
# 0.2.0: writes ./out.csv (relative to where Python runs)
# 0.3.0: writes /data/out.csv (next to in.csv)This raises no error. If your code combines cwd= with a relative stdout_path, its output file will be written somewhere else after you upgrade. To keep the old location, pass an absolute path. (#7)
Fixed
AsyncQsvtimeouts keep stderr.QsvTimeout.stderrnow includes what qsv wrote before the timeout, as it already did forQsv. Previously, stderr already read when the timeout fired was lost. (#6)- No bare
ValueErrorfrom capability detection. If the binary's version can't be determined,capabilities(andrunwithmin_version) raiseQsvError. If--capabilitiesreturns JSON of an unexpected shape or without a usable version, the client falls back to--version. (#7, #8) - Windows:
binary=/QSV_BINpaths without an extension.C:/tools/qsvnow findsC:/tools/qsv.exe, trying eachPATHEXTextension. (#8) - The
QsvErrordocstring now names the argv attribute correctly:args_run. (#7)
Under the hood
- New tests exercise qsv's JSON error format and
--capabilitiesend to end, plus binary discovery. The latest qsv release (24.0.0) supports neither format yet, so these paths were previously only tested at the parser level. - CI also runs the integration tests against a pinned qsv 24.0.0, so the text-error and
--versionfallbacks stay covered once newer qsv releases support JSON. - The publish workflow refuses to upload if the release tag doesn't match the package version.
Full changelog: 0.2.0...0.3.0
0.2.0
Added
- Windows support. qsv is started suspended and placed in its own Job Object before it runs,
so a timeout, cancellation or Ctrl-C stops qsv and everything it started:
CTRL_BREAK_EVENTfirst, thenTerminateJobObjectafterkill_grace. Previously only
qsv.exeitself was killed. Withinherit_env=False,SYSTEMROOTis still passed on
Windows, since many programs fail to start without it. CI runs the full test suite on
Windows. (#4,
#5)
Changed
- The sdist now contains only the package, tests, README, CHANGELOG and LICENSE. 0.1.0's also
included repository tooling files (CLAUDE.md,.serena/,uv.lock,.github/). The wheel
is unchanged. (#2) - Tested on Python 3.14 as well as 3.10–3.13. (#3)
Fixed
- A timeout now raises within a few
kill_graceperiods even when a process qsv started has
left the process group and still holds its output pipes, including when qsv ignores SIGTERM.
Previously the run waited for that process to exit. Output it still holds is dropped, and
AsyncQsvcloses its pipe file descriptors instead of keeping them open until it exits.
(#3) - Cancelling an
AsyncQsvrun now always kills qsv and everything it started, even when the
cancellation arrives during timeout cleanup or comes fromasyncio.run()shutting down. A
cancelled run finishes its teardown before the cancellation propagates.
(#3) - A second Ctrl-C while a sync run is waiting out
kill_graceno longer skips the forced kill,
which left a qsv that ignores the polite stop running. A Ctrl-C right after qsv starts is
handled the same way. (#5) - On Windows with Python 3.10–3.12, a run whose child stops reading a large
stdin=payload
no longer hangs past its timeout. (#5) - A failed run whose stdout is not valid UTF-8 raises the typed
QsvErrorinstead of
UnicodeDecodeError. (#3) count()andheaders()work withstdout_path=andtext=Falseinstead of raising
IndexError. (#3)- A relative
binarypath now works together withcwd=.find_qsvreturns absolute paths.
(#3)
Full Changelog: 0.1.0...0.2.0
0.1.0
Initial qsv-client 0.1.0: sync + asyncio clients for running qsv #1
Full Changelog: https://github.com/dathere/qsv-client/commits/0.1.0