Driver Server client with an embeddable connector for config injection at runtime.
Project description
driverclient
Embeddable client for a Driver Server local repo. Scans a Windows machine's hardware, resolves and installs drivers from the repo, and captures drivers back up to it. Designed to be driven programmatically from a host application (for example a PyQt front-end) via a small connector class.
Install
pip install driverclient
Usage
Create one DriverClient per process and call .run():
from driverclient import DriverClient
client = DriverClient(
local_repo_url="http://REPO_HOST:8000",
node_key="YOUR_NODE_KEY",
)
result = client.run("capture-all") # or client.run() for the configured default
Configuration
Config can be supplied three ways and is merged with this priority (highest wins):
DEFAULTS < DS_CLIENT_CONFIG env file < config_path file < keyword args
- Args only — pass keys directly:
DriverClient(local_repo_url="http://x:1", node_key="k")
- File only — pass a JSON file used as the base:
DriverClient(config_path="/etc/driverclient.json")
- Both — file as the base, individual keys overridden by kwargs:
DriverClient(config_path="/etc/driverclient.json", node_key="override")
Passing nothing preserves the default behavior (env file → packaged
config.json → built-in defaults).
A dummy config.json ships inside the package as a template. Replace
REPLACE_WITH_YOUR_NODE_KEY and point local_repo_url at your repo — or, better,
supply real values at runtime through the connector.
Commands
scan, resolve, resolve-and-install, capture-all, capture-missing,
wu-update, wu-full, automate.
client.run() with no argument uses DS_CLIENT_COMMAND, then the
default_command from config.
Progress events
Every operation reports progress in real time through a structured event stream
(added in 0.2.0). Pass an on_event callback to run() and it is invoked
synchronously for each step:
def on_event(ev): # ev is a driverclient.ClientEvent
print(ev.phase, ev.status, ev.message)
client.run("automate", on_event=on_event)
When no callback is given, events fall back to print + the
driverclient logger, so the terminal/log experience is unchanged.
Each ClientEvent (frozen dataclass, ev.to_dict() for transport) has:
| field | type | meaning |
|---|---|---|
phase |
str | scan resolve install capture windows_update dump upload pipeline done |
status |
str | start progress ok warn error done |
message |
str | human-readable line (what the fallback prints) |
current |
int | None | counter position (e.g. driver 3 of 12) |
total |
int | None | counter total |
percent |
float | None | 0–100 for long single operations |
data |
dict | structured specifics (driver_name, hwid, path, error, …) |
ts |
float | time.time() at emit |
phase and status are a fixed, documented vocabulary — this is the interface
a GUI binds to; treat changes to it as an API change.
Reboots (the client does not reboot — the caller decides)
The client is stateless and single-shot: it installs/captures in one pass and
returns. It never reboots and keeps no state across reboots. When a driver
installs but needs a reboot to take effect (installer exit code 3010/1641),
that is reported as success with a reboot flag, not a failure:
InstallResult.reboot_required/AutomateResult.reboot_required→bool- the per-driver
installevent carriesdata["reboot_required"]
The embedding app owns the reboot + resume loop. Typical convergence:
result = client.run("automate", on_event=cb)
if result.reboot_required:
persist_state(); schedule_resume(); reboot() # then run "automate" again
elif not result.made_progress:
proceed_to_next_step() # nothing new + no reboot = converged
AutomateResult.made_progress is True when the pass installed or captured
anything, so not made_progress and not reboot_required means the machine has
converged. Drivers for devices that only appear after a reboot are picked up on
the next pass; run a capture-missing / wu-full pass post-reboot to catch
packages Windows Update only stages during the reboot.
Events are emitted on whatever thread the op is running on — and the parallel download/export/upload pools mean some events arrive on worker threads. The Qt pattern below (queued cross-thread signals) is safe regardless.
PyQt consumer pattern
Run the (minutes-long, blocking) client.run(...) on a QThread, never the UI
thread. The on_event callback must not touch widgets — it emits a Qt signal
carrying event.to_dict(); a slot on the UI thread updates the widgets. Qt
delivers cross-thread signals via the receiver's event loop (queued), which is
the thread-safe way to drive the UI.
from PyQt6.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
from driverclient import DriverClient
class Worker(QObject):
event = pyqtSignal(dict) # carries ClientEvent.to_dict()
finished = pyqtSignal(object) # carries the op's result
def __init__(self, command):
super().__init__()
self.command = command
@pyqtSlot()
def run(self):
client = DriverClient(local_repo_url="http://REPO_HOST:8000",
node_key="YOUR_NODE_KEY")
# Called on THIS worker thread — only emit a signal, never touch widgets.
result = client.run(self.command, on_event=lambda ev: self.event.emit(ev.to_dict()))
self.finished.emit(result)
class Panel:
"""Wire a worker onto a thread and bind its signals to UI-thread slots."""
def start(self, command):
self.thread = QThread()
self.worker = Worker(command)
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
self.worker.event.connect(self.on_event) # queued → runs on UI thread
self.worker.finished.connect(self.on_finished)
self.thread.start()
@pyqtSlot(dict)
def on_event(self, ev):
# Safe: this runs on the UI thread. Update widgets here.
self.status_label.setText(ev["message"])
if ev["phase"] == "install" and ev["status"] in ("ok", "error"):
self.step_list.addItem(ev["message"])
if ev["total"] and ev["current"] is not None:
self.progress_bar.setMaximum(ev["total"])
self.progress_bar.setValue(ev["current"])
elif ev["percent"] is not None:
self.progress_bar.setValue(int(ev["percent"]))
@pyqtSlot(object)
def on_finished(self, result):
self.thread.quit()
self.thread.wait()
Requirements
- Python >= 3.10
- Windows target for the actual driver operations (
pnputil, WMI, DISM).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file driverclient-0.2.11.tar.gz.
File metadata
- Download URL: driverclient-0.2.11.tar.gz
- Upload date:
- Size: 40.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8ff53e36758e7713b3a6018f082c266aacc1042f346889b3f5c405d0428d80b
|
|
| MD5 |
a08aa551aa14ad5f98b928f65b47015c
|
|
| BLAKE2b-256 |
cc8133304cd8b8aa7814a8c89d91a119ea42f1e91bcf6897e7aedcd39849a576
|
File details
Details for the file driverclient-0.2.11-py3-none-any.whl.
File metadata
- Download URL: driverclient-0.2.11-py3-none-any.whl
- Upload date:
- Size: 48.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3182fffea4ec3f6b9c3831206f4eaf6b5100c0a53c58676445cc1c9cb68f5f12
|
|
| MD5 |
10b09894494a3d716c685011d771f28c
|
|
| BLAKE2b-256 |
c103ffeb358f194092ecc07077e06a2ed2ee63d874e1feb358f7d8d19b65d2e3
|