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.
- driverclient-0.2.0/.gitignore +63 -0
- driverclient-0.2.0/PKG-INFO +179 -0
- driverclient-0.2.0/README.md +163 -0
- driverclient-0.2.0/pyproject.toml +42 -0
- driverclient-0.2.0/src/driverclient/__init__.py +73 -0
- driverclient-0.2.0/src/driverclient/__main__.py +4 -0
- driverclient-0.2.0/src/driverclient/config.json +36 -0
- driverclient-0.2.0/src/driverclient/config.py +98 -0
- driverclient-0.2.0/src/driverclient/core/__init__.py +0 -0
- driverclient-0.2.0/src/driverclient/core/hardware.py +345 -0
- driverclient-0.2.0/src/driverclient/core/http.py +304 -0
- driverclient-0.2.0/src/driverclient/events.py +141 -0
- driverclient-0.2.0/src/driverclient/main.py +71 -0
- driverclient-0.2.0/src/driverclient/ops/__init__.py +15 -0
- driverclient-0.2.0/src/driverclient/ops/automate.py +118 -0
- driverclient-0.2.0/src/driverclient/ops/capture.py +679 -0
- driverclient-0.2.0/src/driverclient/ops/install.py +276 -0
- driverclient-0.2.0/src/driverclient/ops/resolve.py +147 -0
- driverclient-0.2.0/src/driverclient/ops/scan.py +316 -0
- driverclient-0.2.0/src/driverclient/py.typed +0 -0
|
@@ -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,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
|