driverclient 0.2.18__tar.gz → 0.2.19__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.
Files changed (23) hide show
  1. {driverclient-0.2.18 → driverclient-0.2.19}/PKG-INFO +1 -1
  2. {driverclient-0.2.18 → driverclient-0.2.19}/pyproject.toml +1 -1
  3. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/__init__.py +1 -1
  4. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/ops/install.py +54 -9
  5. {driverclient-0.2.18 → driverclient-0.2.19}/.gitignore +0 -0
  6. {driverclient-0.2.18 → driverclient-0.2.19}/README.md +0 -0
  7. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/__main__.py +0 -0
  8. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/config.json +0 -0
  9. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/config.py +0 -0
  10. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/core/__init__.py +0 -0
  11. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/core/hardware.py +0 -0
  12. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/core/http.py +0 -0
  13. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/core/hwid.py +0 -0
  14. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/core/proc.py +0 -0
  15. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/events.py +0 -0
  16. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/main.py +0 -0
  17. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/ops/__init__.py +0 -0
  18. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/ops/automate.py +0 -0
  19. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/ops/capture.py +0 -0
  20. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/ops/diagnose.py +0 -0
  21. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/ops/resolve.py +0 -0
  22. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/ops/scan.py +0 -0
  23. {driverclient-0.2.18 → driverclient-0.2.19}/src/driverclient/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: driverclient
3
- Version: 0.2.18
3
+ Version: 0.2.19
4
4
  Summary: Driver Server client with an embeddable connector for config injection at runtime.
5
5
  Project-URL: Homepage, https://example.com/driverclient
6
6
  Author-email: Raja Sanaullah <sanaullah@99technologies.com>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "driverclient"
7
- version = "0.2.18"
7
+ version = "0.2.19"
8
8
  description = "Driver Server client with an embeddable connector for config injection at runtime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -25,7 +25,7 @@ from driverclient.main import run as _run
25
25
 
26
26
  __all__ = ["DriverClient", "ClientEvent"]
27
27
 
28
- __version__ = "0.2.18"
28
+ __version__ = "0.2.19"
29
29
 
30
30
 
31
31
  class DriverClient:
@@ -68,6 +68,7 @@ class DriverResult:
68
68
  error: str = ""
69
69
  duration_ms: int = 0
70
70
  reboot_required: bool = False # installed OK but needs a reboot (pnputil 3010)
71
+ skipped: bool = False # already present in the driver store — not re-fetched/installed
71
72
 
72
73
 
73
74
  @dataclass
@@ -154,9 +155,17 @@ def resolve_and_install(force: bool = False) -> InstallResult:
154
155
  # at the same-or-newer version — so a still-current big driver (graphics) is not
155
156
  # re-downloaded, while missing/extension drivers still install. Toggle off with
156
157
  # skip_installed_drivers=false to force-reinstall everything.
158
+ # Live driver-store index: INF name -> highest installed version. Built once,
159
+ # then UPDATED as each driver installs this pass (see _record_installed) so a
160
+ # later package of the same driver family (same INF) at an equal-or-older
161
+ # version is skipped — the driver store already has it. This is skip-if-present
162
+ # (idempotency), NOT deduplication: no repo package is removed; a newer repo
163
+ # version still installs over an older store version (store_ver >= repo_ver skips).
164
+ skip_installed = cfg.get("skip_installed_drivers", True)
165
+ idx: dict = _installed_inf_index() if skip_installed else {}
166
+
157
167
  drivers = result.drivers
158
- if cfg.get("skip_installed_drivers", True):
159
- idx = _installed_inf_index()
168
+ if skip_installed:
160
169
  kept = [d for d in drivers if not _already_in_store(d, idx)]
161
170
  skipped = len(drivers) - len(kept)
162
171
  if skipped:
@@ -179,21 +188,24 @@ def resolve_and_install(force: bool = False) -> InstallResult:
179
188
  events.start("install",
180
189
  f"[install] {len(drivers)} driver(s) — starting parallel downloads…",
181
190
  total=len(drivers))
182
- all_results = _stream_install(drivers, work_dir, cfg)
191
+ all_results = _stream_install(drivers, work_dir, cfg, idx, skip_installed)
183
192
 
184
193
  shutil.rmtree(work_dir, ignore_errors=True)
185
194
 
186
- installed_ok = [r for r in all_results if r.success]
195
+ installed_ok = [r for r in all_results if r.success and not r.skipped]
196
+ skipped_now = [r for r in all_results if r.skipped]
187
197
  install_failed = [r for r in all_results if not r.success]
188
198
 
189
- _confirm(result.trace_id, installed_ok, install_failed, cfg)
199
+ # Skipped drivers ARE satisfied on the machine — report them upstream as success.
200
+ _confirm(result.trace_id, installed_ok + skipped_now, install_failed, cfg)
190
201
 
191
202
  reboot_required = any(r.reboot_required for r in installed_ok)
192
203
  reboot_tag = " — reboot required" if reboot_required else ""
204
+ skip_tag = f" ({len(skipped_now)} already in store)" if skipped_now else ""
193
205
  events.done("install",
194
- f"[install] Done — {len(installed_ok)} ok {len(install_failed)} failed{reboot_tag}",
206
+ f"[install] Done — {len(installed_ok)} ok {len(install_failed)} failed{skip_tag}{reboot_tag}",
195
207
  total=len(all_results), ok=len(installed_ok), failed=len(install_failed),
196
- reboot_required=reboot_required)
208
+ skipped=len(skipped_now), reboot_required=reboot_required)
197
209
  for r in install_failed:
198
210
  events.error("install", f" ✗ {r.binding_id}: {r.error}",
199
211
  binding_id=r.binding_id, error=r.error)
@@ -207,10 +219,16 @@ def resolve_and_install(force: bool = False) -> InstallResult:
207
219
 
208
220
  # ── Streaming pipeline ──────────────────────────────────────────────────────────
209
221
 
210
- def _stream_install(drivers: list[dict], base: Path, cfg: dict) -> list[DriverResult]:
222
+ def _stream_install(drivers: list[dict], base: Path, cfg: dict,
223
+ idx: dict, skip_installed: bool) -> list[DriverResult]:
211
224
  """
212
225
  Submit ALL downloads (all levels) simultaneously, then install level-by-level.
213
226
  Each driver installs the moment its own download Future completes.
227
+
228
+ `idx` is the live driver-store index (INF -> installed version); it is read to
229
+ skip a driver the store already has and updated after each successful install
230
+ so same-family duplicates later in the pass are skipped too. Installs run one
231
+ at a time here (main thread), so mutating `idx` needs no lock.
214
232
  """
215
233
  repo_url = cfg["local_repo_url"]
216
234
  parallel = cfg.get("parallel_downloads", 6)
@@ -225,7 +243,7 @@ def _stream_install(drivers: list[dict], base: Path, cfg: dict) -> list[DriverRe
225
243
  f"[install] {_level_label(level)} ({len(level_futs[level])} driver(s))…",
226
244
  category=_level_label(level), count=len(level_futs[level]))
227
245
  for fut in as_completed(level_futs[level]):
228
- all_results.append(_process_completed(fut, future_info, cfg))
246
+ all_results.append(_process_completed(fut, future_info, cfg, idx, skip_installed))
229
247
 
230
248
  executor.shutdown(wait=False)
231
249
  return all_results
@@ -266,12 +284,23 @@ def _process_completed(
266
284
  fut: Future,
267
285
  future_info: dict[Future, tuple[int, dict]],
268
286
  cfg: dict,
287
+ idx: dict,
288
+ skip_installed: bool,
269
289
  ) -> DriverResult:
270
290
  """Handle one completed download Future: install if ok, return DriverResult."""
271
291
  _, driver = future_info[fut]
272
292
  bid = driver.get("binding_id", "")
273
293
  cat = driver.get("category", "other")
274
294
 
295
+ # A driver installed earlier THIS pass may have added the same INF (another
296
+ # package of the same driver family), so a package that was missing at
297
+ # pass-start can now already be in the store at an equal-or-newer version.
298
+ # Re-check against the live index and skip the redundant re-install.
299
+ if skip_installed and _already_in_store(driver, idx):
300
+ events.ok("install", f" ⭑ [{cat}] {bid} already in driver store — skipped",
301
+ binding_id=bid, category=cat, skipped=True)
302
+ return DriverResult(binding_id=bid, success=True, skipped=True)
303
+
275
304
  try:
276
305
  dest, dl_ok = fut.result()
277
306
  except Exception as e:
@@ -283,6 +312,8 @@ def _process_completed(
283
312
  t0 = time.monotonic()
284
313
  ok, err, reboot = _install_driver(driver, dest, cfg)
285
314
  dur = int((time.monotonic() - t0) * 1000)
315
+ if ok:
316
+ _record_installed(driver, idx) # update live index so same-family dupes skip
286
317
  tag = " (reboot required)" if reboot else ""
287
318
  line = f" {'✓' if ok else '✗'} [{cat}] {bid} ({dur}ms){tag}"
288
319
  emit = events.ok if ok else events.error
@@ -292,6 +323,20 @@ def _process_completed(
292
323
  duration_ms=dur, reboot_required=reboot)
293
324
 
294
325
 
326
+ def _record_installed(driver: dict, idx: dict) -> None:
327
+ """Fold a just-installed driver's INF(s)+version into the live store index, so a
328
+ later package of the same driver family at an equal-or-older version is skipped
329
+ this pass — mirrors what `pnputil /enum-drivers` will report on the next scan."""
330
+ ver = _parse_ver(driver.get("version_label", ""))
331
+ if not ver:
332
+ return
333
+ for f in driver.get("files", []):
334
+ name = f.get("filename", "").lower()
335
+ if f.get("file_role") == "inf" or name.endswith(".inf"):
336
+ if idx.get(name) is None or ver > idx[name]:
337
+ idx[name] = ver
338
+
339
+
295
340
  def _download_driver(driver: dict, base: Path, repo_url: str,
296
341
  cfg: dict) -> tuple[Path, bool]:
297
342
  """Download all files for one driver. Returns (dest_dir, success)."""
File without changes
File without changes