driverclient 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ venv/
6
+ .venv/
7
+ *.egg-info/
8
+
9
+ # Real configs — never commit (contain secrets or real node values)
10
+ backend/config.json
11
+ backend/.env
12
+ frontend/.env
13
+ configs/keys.json
14
+
15
+ # Secrets fallback
16
+ *.env
17
+ .env
18
+ .env.*
19
+ # Committed safe dev defaults
20
+ !backend/.env.development
21
+ !frontend/.env.development
22
+ # Committed fill-in templates (no real values)
23
+ !configs/templates/**/.env
24
+ !configs/templates/**/config.json
25
+
26
+ # Frontend
27
+ frontend/node_modules/
28
+ frontend/dist/
29
+
30
+ # Runtime data
31
+ data/
32
+ dist/
33
+ dev_data/
34
+ driverserver-data/
35
+ ds_download/
36
+ ds_capture_temp/
37
+ ds_not_found.json
38
+ ds_wu_tried.json
39
+ ds_cache.json
40
+
41
+ # Alembic / SQLite local dev databases
42
+ *.db
43
+ *.db-shm
44
+ *.db-wal
45
+
46
+ # Celery beat schedule file
47
+ celerybeat-schedule
48
+ celerybeat.pid
49
+
50
+ # IDE — launch.json is developer-specific; add .vscode/settings.json exceptions if needed
51
+ .vscode/launch.json
52
+ .idea/
53
+ *.swp
54
+
55
+ # Deploy vendor binaries (download separately — see deploy/vendor/README.md)
56
+ deploy/vendor/nssm/
57
+ deploy/vendor/nginx/
58
+
59
+ # Deploy bundles (generated output)
60
+ deploy/bundles/
61
+
62
+ # Claude Code
63
+ .claude/
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: driverclient
3
+ Version: 0.2.0
4
+ Summary: Driver Server client with an embeddable connector for config injection at runtime.
5
+ Project-URL: Homepage, https://example.com/driverclient
6
+ Author-email: Raja Sanaullah <sanaullah@99technologies.com>
7
+ License-Expression: MIT
8
+ Keywords: deployment,driver,pnputil,pxe,windows
9
+ Classifier: Intended Audience :: System Administrators
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: zstandard>=0.22.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # driverclient
18
+
19
+ Embeddable client for a Driver Server local repo. Scans a Windows machine's
20
+ hardware, resolves and installs drivers from the repo, and captures drivers
21
+ back up to it. Designed to be driven programmatically from a host application
22
+ (for example a PyQt front-end) via a small connector class.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install driverclient
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ Create one `DriverClient` per process and call `.run()`:
33
+
34
+ ```python
35
+ from driverclient import DriverClient
36
+
37
+ client = DriverClient(
38
+ local_repo_url="http://REPO_HOST:8000",
39
+ node_key="YOUR_NODE_KEY",
40
+ )
41
+ result = client.run("capture-all") # or client.run() for the configured default
42
+ ```
43
+
44
+ ### Configuration
45
+
46
+ Config can be supplied three ways and is merged with this priority
47
+ (**highest wins**):
48
+
49
+ ```
50
+ DEFAULTS < DS_CLIENT_CONFIG env file < config_path file < keyword args
51
+ ```
52
+
53
+ - **Args only** — pass keys directly:
54
+ ```python
55
+ DriverClient(local_repo_url="http://x:1", node_key="k")
56
+ ```
57
+ - **File only** — pass a JSON file used as the base:
58
+ ```python
59
+ DriverClient(config_path="/etc/driverclient.json")
60
+ ```
61
+ - **Both** — file as the base, individual keys overridden by kwargs:
62
+ ```python
63
+ DriverClient(config_path="/etc/driverclient.json", node_key="override")
64
+ ```
65
+
66
+ Passing nothing preserves the default behavior (env file → packaged
67
+ `config.json` → built-in defaults).
68
+
69
+ A dummy `config.json` ships inside the package as a template. Replace
70
+ `REPLACE_WITH_YOUR_NODE_KEY` and point `local_repo_url` at your repo — or, better,
71
+ supply real values at runtime through the connector.
72
+
73
+ ### Commands
74
+
75
+ `scan`, `resolve`, `resolve-and-install`, `capture-all`, `capture-missing`,
76
+ `wu-update`, `wu-full`, `automate`.
77
+
78
+ `client.run()` with no argument uses `DS_CLIENT_COMMAND`, then the
79
+ `default_command` from config.
80
+
81
+ ### Progress events
82
+
83
+ Every operation reports progress in real time through a structured event stream
84
+ (added in **0.2.0**). Pass an `on_event` callback to `run()` and it is invoked
85
+ synchronously for each step:
86
+
87
+ ```python
88
+ def on_event(ev): # ev is a driverclient.ClientEvent
89
+ print(ev.phase, ev.status, ev.message)
90
+
91
+ client.run("automate", on_event=on_event)
92
+ ```
93
+
94
+ When no callback is given, events fall back to `print` + the
95
+ `driverclient` logger, so the terminal/log experience is unchanged.
96
+
97
+ Each `ClientEvent` (frozen dataclass, `ev.to_dict()` for transport) has:
98
+
99
+ | field | type | meaning |
100
+ |-----------|-----------------|-----------------------------------------------------|
101
+ | `phase` | str | `scan` `resolve` `install` `capture` `windows_update` `dump` `upload` `pipeline` `done` |
102
+ | `status` | str | `start` `progress` `ok` `warn` `error` `done` |
103
+ | `message` | str | human-readable line (what the fallback prints) |
104
+ | `current` | int \| None | counter position (e.g. driver 3 of 12) |
105
+ | `total` | int \| None | counter total |
106
+ | `percent` | float \| None | 0–100 for long single operations |
107
+ | `data` | dict | structured specifics (`driver_name`, `hwid`, `path`, `error`, …) |
108
+ | `ts` | float | `time.time()` at emit |
109
+
110
+ `phase` and `status` are a fixed, documented vocabulary — this is the interface
111
+ a GUI binds to; treat changes to it as an API change.
112
+
113
+ > Events are emitted on whatever thread the op is running on — and the parallel
114
+ > download/export/upload pools mean some events arrive on **worker threads**. The
115
+ > Qt pattern below (queued cross-thread signals) is safe regardless.
116
+
117
+ #### PyQt consumer pattern
118
+
119
+ Run the (minutes-long, blocking) `client.run(...)` on a **QThread**, never the UI
120
+ thread. The `on_event` callback must **not** touch widgets — it emits a Qt signal
121
+ carrying `event.to_dict()`; a slot on the UI thread updates the widgets. Qt
122
+ delivers cross-thread signals via the receiver's event loop (queued), which is
123
+ the thread-safe way to drive the UI.
124
+
125
+ ```python
126
+ from PyQt6.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
127
+ from driverclient import DriverClient
128
+
129
+
130
+ class Worker(QObject):
131
+ event = pyqtSignal(dict) # carries ClientEvent.to_dict()
132
+ finished = pyqtSignal(object) # carries the op's result
133
+
134
+ def __init__(self, command):
135
+ super().__init__()
136
+ self.command = command
137
+
138
+ @pyqtSlot()
139
+ def run(self):
140
+ client = DriverClient(local_repo_url="http://REPO_HOST:8000",
141
+ node_key="YOUR_NODE_KEY")
142
+ # Called on THIS worker thread — only emit a signal, never touch widgets.
143
+ result = client.run(self.command, on_event=lambda ev: self.event.emit(ev.to_dict()))
144
+ self.finished.emit(result)
145
+
146
+
147
+ class Panel:
148
+ """Wire a worker onto a thread and bind its signals to UI-thread slots."""
149
+ def start(self, command):
150
+ self.thread = QThread()
151
+ self.worker = Worker(command)
152
+ self.worker.moveToThread(self.thread)
153
+ self.thread.started.connect(self.worker.run)
154
+ self.worker.event.connect(self.on_event) # queued → runs on UI thread
155
+ self.worker.finished.connect(self.on_finished)
156
+ self.thread.start()
157
+
158
+ @pyqtSlot(dict)
159
+ def on_event(self, ev):
160
+ # Safe: this runs on the UI thread. Update widgets here.
161
+ self.status_label.setText(ev["message"])
162
+ if ev["phase"] == "install" and ev["status"] in ("ok", "error"):
163
+ self.step_list.addItem(ev["message"])
164
+ if ev["total"] and ev["current"] is not None:
165
+ self.progress_bar.setMaximum(ev["total"])
166
+ self.progress_bar.setValue(ev["current"])
167
+ elif ev["percent"] is not None:
168
+ self.progress_bar.setValue(int(ev["percent"]))
169
+
170
+ @pyqtSlot(object)
171
+ def on_finished(self, result):
172
+ self.thread.quit()
173
+ self.thread.wait()
174
+ ```
175
+
176
+ ## Requirements
177
+
178
+ - Python >= 3.10
179
+ - Windows target for the actual driver operations (`pnputil`, WMI, DISM).
@@ -0,0 +1,163 @@
1
+ # driverclient
2
+
3
+ Embeddable client for a Driver Server local repo. Scans a Windows machine's
4
+ hardware, resolves and installs drivers from the repo, and captures drivers
5
+ back up to it. Designed to be driven programmatically from a host application
6
+ (for example a PyQt front-end) via a small connector class.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install driverclient
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ Create one `DriverClient` per process and call `.run()`:
17
+
18
+ ```python
19
+ from driverclient import DriverClient
20
+
21
+ client = DriverClient(
22
+ local_repo_url="http://REPO_HOST:8000",
23
+ node_key="YOUR_NODE_KEY",
24
+ )
25
+ result = client.run("capture-all") # or client.run() for the configured default
26
+ ```
27
+
28
+ ### Configuration
29
+
30
+ Config can be supplied three ways and is merged with this priority
31
+ (**highest wins**):
32
+
33
+ ```
34
+ DEFAULTS < DS_CLIENT_CONFIG env file < config_path file < keyword args
35
+ ```
36
+
37
+ - **Args only** — pass keys directly:
38
+ ```python
39
+ DriverClient(local_repo_url="http://x:1", node_key="k")
40
+ ```
41
+ - **File only** — pass a JSON file used as the base:
42
+ ```python
43
+ DriverClient(config_path="/etc/driverclient.json")
44
+ ```
45
+ - **Both** — file as the base, individual keys overridden by kwargs:
46
+ ```python
47
+ DriverClient(config_path="/etc/driverclient.json", node_key="override")
48
+ ```
49
+
50
+ Passing nothing preserves the default behavior (env file → packaged
51
+ `config.json` → built-in defaults).
52
+
53
+ A dummy `config.json` ships inside the package as a template. Replace
54
+ `REPLACE_WITH_YOUR_NODE_KEY` and point `local_repo_url` at your repo — or, better,
55
+ supply real values at runtime through the connector.
56
+
57
+ ### Commands
58
+
59
+ `scan`, `resolve`, `resolve-and-install`, `capture-all`, `capture-missing`,
60
+ `wu-update`, `wu-full`, `automate`.
61
+
62
+ `client.run()` with no argument uses `DS_CLIENT_COMMAND`, then the
63
+ `default_command` from config.
64
+
65
+ ### Progress events
66
+
67
+ Every operation reports progress in real time through a structured event stream
68
+ (added in **0.2.0**). Pass an `on_event` callback to `run()` and it is invoked
69
+ synchronously for each step:
70
+
71
+ ```python
72
+ def on_event(ev): # ev is a driverclient.ClientEvent
73
+ print(ev.phase, ev.status, ev.message)
74
+
75
+ client.run("automate", on_event=on_event)
76
+ ```
77
+
78
+ When no callback is given, events fall back to `print` + the
79
+ `driverclient` logger, so the terminal/log experience is unchanged.
80
+
81
+ Each `ClientEvent` (frozen dataclass, `ev.to_dict()` for transport) has:
82
+
83
+ | field | type | meaning |
84
+ |-----------|-----------------|-----------------------------------------------------|
85
+ | `phase` | str | `scan` `resolve` `install` `capture` `windows_update` `dump` `upload` `pipeline` `done` |
86
+ | `status` | str | `start` `progress` `ok` `warn` `error` `done` |
87
+ | `message` | str | human-readable line (what the fallback prints) |
88
+ | `current` | int \| None | counter position (e.g. driver 3 of 12) |
89
+ | `total` | int \| None | counter total |
90
+ | `percent` | float \| None | 0–100 for long single operations |
91
+ | `data` | dict | structured specifics (`driver_name`, `hwid`, `path`, `error`, …) |
92
+ | `ts` | float | `time.time()` at emit |
93
+
94
+ `phase` and `status` are a fixed, documented vocabulary — this is the interface
95
+ a GUI binds to; treat changes to it as an API change.
96
+
97
+ > Events are emitted on whatever thread the op is running on — and the parallel
98
+ > download/export/upload pools mean some events arrive on **worker threads**. The
99
+ > Qt pattern below (queued cross-thread signals) is safe regardless.
100
+
101
+ #### PyQt consumer pattern
102
+
103
+ Run the (minutes-long, blocking) `client.run(...)` on a **QThread**, never the UI
104
+ thread. The `on_event` callback must **not** touch widgets — it emits a Qt signal
105
+ carrying `event.to_dict()`; a slot on the UI thread updates the widgets. Qt
106
+ delivers cross-thread signals via the receiver's event loop (queued), which is
107
+ the thread-safe way to drive the UI.
108
+
109
+ ```python
110
+ from PyQt6.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
111
+ from driverclient import DriverClient
112
+
113
+
114
+ class Worker(QObject):
115
+ event = pyqtSignal(dict) # carries ClientEvent.to_dict()
116
+ finished = pyqtSignal(object) # carries the op's result
117
+
118
+ def __init__(self, command):
119
+ super().__init__()
120
+ self.command = command
121
+
122
+ @pyqtSlot()
123
+ def run(self):
124
+ client = DriverClient(local_repo_url="http://REPO_HOST:8000",
125
+ node_key="YOUR_NODE_KEY")
126
+ # Called on THIS worker thread — only emit a signal, never touch widgets.
127
+ result = client.run(self.command, on_event=lambda ev: self.event.emit(ev.to_dict()))
128
+ self.finished.emit(result)
129
+
130
+
131
+ class Panel:
132
+ """Wire a worker onto a thread and bind its signals to UI-thread slots."""
133
+ def start(self, command):
134
+ self.thread = QThread()
135
+ self.worker = Worker(command)
136
+ self.worker.moveToThread(self.thread)
137
+ self.thread.started.connect(self.worker.run)
138
+ self.worker.event.connect(self.on_event) # queued → runs on UI thread
139
+ self.worker.finished.connect(self.on_finished)
140
+ self.thread.start()
141
+
142
+ @pyqtSlot(dict)
143
+ def on_event(self, ev):
144
+ # Safe: this runs on the UI thread. Update widgets here.
145
+ self.status_label.setText(ev["message"])
146
+ if ev["phase"] == "install" and ev["status"] in ("ok", "error"):
147
+ self.step_list.addItem(ev["message"])
148
+ if ev["total"] and ev["current"] is not None:
149
+ self.progress_bar.setMaximum(ev["total"])
150
+ self.progress_bar.setValue(ev["current"])
151
+ elif ev["percent"] is not None:
152
+ self.progress_bar.setValue(int(ev["percent"]))
153
+
154
+ @pyqtSlot(object)
155
+ def on_finished(self, result):
156
+ self.thread.quit()
157
+ self.thread.wait()
158
+ ```
159
+
160
+ ## Requirements
161
+
162
+ - Python >= 3.10
163
+ - Windows target for the actual driver operations (`pnputil`, WMI, DISM).
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "driverclient"
7
+ version = "0.2.0"
8
+ description = "Driver Server client with an embeddable connector for config injection at runtime."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Raja Sanaullah", email = "sanaullah@99technologies.com" },
14
+ ]
15
+ keywords = ["driver", "windows", "pnputil", "deployment", "pxe"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Operating System :: Microsoft :: Windows",
20
+ "Intended Audience :: System Administrators",
21
+ ]
22
+ dependencies = [
23
+ "zstandard>=0.22.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://example.com/driverclient"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/driverclient"]
31
+ # Ship non-Python package data (config template + type marker) inside the wheel.
32
+ artifacts = [
33
+ "src/driverclient/config.json",
34
+ "src/driverclient/py.typed",
35
+ ]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ include = [
39
+ "src/driverclient",
40
+ "README.md",
41
+ "pyproject.toml",
42
+ ]
@@ -0,0 +1,73 @@
1
+ """
2
+ driverclient — Driver Server client, packaged for embedding.
3
+
4
+ Typical use from an embedding app (e.g. a PyQt front-end):
5
+
6
+ from driverclient import DriverClient
7
+
8
+ # args only
9
+ client = DriverClient(local_repo_url="http://REPO_HOST:8000", node_key="...")
10
+
11
+ # file only
12
+ client = DriverClient(config_path="/etc/driverclient.json")
13
+
14
+ # file as base, individual keys overridden by kwargs
15
+ client = DriverClient(config_path="/etc/driverclient.json", node_key="...")
16
+
17
+ result = client.run("capture-all") # or client.run() for default_command
18
+
19
+ Config merge priority (highest wins):
20
+ DEFAULTS < DS_CLIENT_CONFIG env file < config_path file < kwargs
21
+ """
22
+ from driverclient import config as _config
23
+ from driverclient.events import ClientEvent, using_sink
24
+ from driverclient.main import run as _run
25
+
26
+ __all__ = ["DriverClient", "ClientEvent"]
27
+
28
+ __version__ = "0.2.0"
29
+
30
+
31
+ class DriverClient:
32
+ """
33
+ Configures the process-global client config, then runs operations.
34
+
35
+ A single active client per process is supported: constructing a
36
+ DriverClient resets and reloads the shared global config singleton.
37
+ Construct one instance and reuse it; constructing another replaces
38
+ the active configuration.
39
+ """
40
+
41
+ def __init__(self, config_path: str | None = None, **kwargs) -> None:
42
+ """
43
+ Args:
44
+ config_path: optional path to a JSON config file used as the base.
45
+ If omitted, the DS_CLIENT_CONFIG env file or the
46
+ packaged config.json is used (existing behavior).
47
+ **kwargs: individual config keys that override everything else,
48
+ including values from config_path.
49
+ """
50
+ _config.reset()
51
+ self.config = _config.load(path=config_path, overrides=kwargs)
52
+
53
+ def run(self, command: str | None = None, on_event=None) -> object:
54
+ """
55
+ Run a client operation and return its result object.
56
+
57
+ Args:
58
+ command: one of the client operations (e.g. "scan", "capture-all",
59
+ "automate"). If None, falls back to DS_CLIENT_COMMAND then
60
+ the default_command from config.
61
+ on_event: optional callback ``Callable[[ClientEvent], None]`` invoked
62
+ synchronously, in real time, for every progress event the
63
+ operation emits. Installed only for the duration of this
64
+ run (see events.using_sink) and restored afterwards. When
65
+ omitted, events fall back to print + logging so the
66
+ terminal experience is unchanged.
67
+
68
+ Returns:
69
+ The operation's result object (ScanResult, InstallResult, ...).
70
+ """
71
+ self.on_event = on_event
72
+ with using_sink(on_event):
73
+ return _run(command)
@@ -0,0 +1,4 @@
1
+ """Allow: python -m driverclient"""
2
+ from driverclient.main import run
3
+
4
+ run()
@@ -0,0 +1,36 @@
1
+ {
2
+ "local_repo_url": "http://localhost:8000",
3
+ "node_key": "REPLACE_WITH_YOUR_NODE_KEY",
4
+
5
+ "default_command": "automate",
6
+
7
+ "parallel_downloads": 6,
8
+ "parallel_uploads": 8,
9
+ "parallel_exports": 4,
10
+
11
+ "driver_root": "C:\\DriverServer",
12
+ "cache_dir": "C:\\DriverServer\\client",
13
+ "work_dir": "C:\\DriverServer\\client\\staging",
14
+ "cache_ttl_minutes": 15,
15
+
16
+ "dump_extensions": [".inf", ".sys", ".cat"],
17
+ "dump_extensions_overrides": {
18
+ "system": [".inf", ".sys", ".cat", ".dll"],
19
+ "usb": [".inf", ".sys", ".cat", ".dll"]
20
+ },
21
+
22
+ "wu_wait_minutes": 10,
23
+ "wu_poll_interval_seconds": 30,
24
+ "wu_ignore_classes": ["hidclass", "bluetooth", "printer"],
25
+
26
+ "automate_wu_fallback": true,
27
+ "automate_re_resolve": false,
28
+ "force_resolve": false,
29
+
30
+ "timeout_tiny": 10,
31
+ "timeout_short": 30,
32
+ "timeout_medium": 60,
33
+ "timeout_long": 120,
34
+ "timeout_install": 600,
35
+ "timeout_wmi_slow": 90
36
+ }
@@ -0,0 +1,98 @@
1
+ """
2
+ driverclient/config.py — Load client settings.
3
+
4
+ Merge priority (highest wins):
5
+ DEFAULTS < DS_CLIENT_CONFIG env file < file passed to load() < overrides
6
+ """
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+
11
+ DEFAULTS: dict = {
12
+ # ── Server ────────────────────────────────────────────────────────────
13
+ "local_repo_url": "http://localhost:8000",
14
+ "node_key": "",
15
+
16
+ # ── Command ───────────────────────────────────────────────────────────
17
+ "default_command": "automate",
18
+
19
+ # ── Parallelism ───────────────────────────────────────────────────────
20
+ "parallel_downloads": 6,
21
+ "parallel_uploads": 8,
22
+ "parallel_exports": 4,
23
+
24
+ # ── Paths ─────────────────────────────────────────────────────────────
25
+ "driver_root": "C:\\DriverServer",
26
+ "cache_dir": "C:\\DriverServer\\client",
27
+ "work_dir": "C:\\DriverServer\\client\\staging",
28
+
29
+ # ── Cache TTL ─────────────────────────────────────────────────────────
30
+ # ds_scan.json / ds_resolve.json are reused if younger than this many minutes
31
+ "cache_ttl_minutes": 15,
32
+
33
+ # ── Capture ───────────────────────────────────────────────────────────
34
+ # [".inf", ".sys", ".cat"] — minimum for server to redistribute and install
35
+ # [".inf", ".sys", ".cat", ".dll"] — adds co-installer DLLs
36
+ # ["*"] — all files in the driver package directory
37
+ "dump_extensions": [".inf", ".sys", ".cat"],
38
+
39
+ # Per-category extension overrides — key = class_name from pnputil (lowercase)
40
+ # Categories not listed here use dump_extensions above
41
+ "dump_extensions_overrides": {
42
+ "system": [".inf", ".sys", ".cat", ".dll"],
43
+ "usb": [".inf", ".sys", ".cat", ".dll"]
44
+ },
45
+
46
+ # ── Windows Update ────────────────────────────────────────────────────
47
+ "wu_wait_minutes": 10,
48
+ "wu_poll_interval_seconds": 30,
49
+ # Classes to skip when deciding which not-found HWIDs trigger WU.
50
+ # Empty list = no filtering (default — make no assumptions).
51
+ # Use lowercase class names as reported by pnputil, e.g. "hidclass", "bluetooth".
52
+ "wu_ignore_classes": ["hidclass"],
53
+
54
+ # ── Automate flags ────────────────────────────────────────────────────
55
+ "automate_wu_fallback": True,
56
+ "automate_re_resolve": False,
57
+ "force_resolve": False,
58
+
59
+ # ── HTTP timeouts (seconds) ───────────────────────────────────────────
60
+ "timeout_tiny": 10,
61
+ "timeout_short": 30,
62
+ "timeout_medium": 60,
63
+ "timeout_long": 120,
64
+ "timeout_install": 600,
65
+ "timeout_wmi_slow": 90,
66
+ }
67
+
68
+ _config: dict | None = None
69
+
70
+
71
+ def load(path: str | None = None, overrides: dict | None = None) -> dict:
72
+ global _config
73
+ if _config is not None:
74
+ return _config
75
+
76
+ if path:
77
+ config_path = Path(path)
78
+ elif "DS_CLIENT_CONFIG" in os.environ:
79
+ config_path = Path(os.environ["DS_CLIENT_CONFIG"])
80
+ else:
81
+ config_path = Path(__file__).parent / "config.json"
82
+
83
+ data: dict = {}
84
+ if config_path.exists():
85
+ with open(config_path) as fh:
86
+ data = json.load(fh)
87
+
88
+ _config = {**DEFAULTS, **data, **(overrides or {})}
89
+ return _config
90
+
91
+
92
+ def get() -> dict:
93
+ return load()
94
+
95
+
96
+ def reset() -> None:
97
+ global _config
98
+ _config = None
File without changes