driverclient 0.2.12__tar.gz → 0.2.13__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.12 → driverclient-0.2.13}/PKG-INFO +1 -1
  2. {driverclient-0.2.12 → driverclient-0.2.13}/pyproject.toml +1 -1
  3. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/__init__.py +1 -1
  4. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/config.py +6 -0
  5. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/ops/install.py +76 -3
  6. {driverclient-0.2.12 → driverclient-0.2.13}/.gitignore +0 -0
  7. {driverclient-0.2.12 → driverclient-0.2.13}/README.md +0 -0
  8. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/__main__.py +0 -0
  9. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/config.json +0 -0
  10. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/core/__init__.py +0 -0
  11. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/core/hardware.py +0 -0
  12. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/core/http.py +0 -0
  13. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/core/hwid.py +0 -0
  14. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/core/proc.py +0 -0
  15. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/events.py +0 -0
  16. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/main.py +0 -0
  17. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/ops/__init__.py +0 -0
  18. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/ops/automate.py +0 -0
  19. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/ops/capture.py +0 -0
  20. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/ops/diagnose.py +0 -0
  21. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/ops/resolve.py +0 -0
  22. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/ops/scan.py +0 -0
  23. {driverclient-0.2.12 → driverclient-0.2.13}/src/driverclient/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: driverclient
3
- Version: 0.2.12
3
+ Version: 0.2.13
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.12"
7
+ version = "0.2.13"
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.12"
28
+ __version__ = "0.2.13"
29
29
 
30
30
 
31
31
  class DriverClient:
@@ -70,6 +70,12 @@ DEFAULTS: dict = {
70
70
  # no new repo drivers or installs nothing.
71
71
  "automate_rescan_passes": 3,
72
72
  "force_resolve": False,
73
+ # The server now offers every driver matching each device (primary + extensions
74
+ # + companions). Before downloading each, skip it if its INF is already in the
75
+ # Windows driver store at the same-or-newer version — so a still-current big
76
+ # driver (e.g. graphics) is never re-downloaded, while missing/extension drivers
77
+ # still install. Set False to force-reinstall everything the server offers.
78
+ "skip_installed_drivers": True,
73
79
 
74
80
  # ── HTTP timeouts (seconds) ───────────────────────────────────────────
75
81
  "timeout_tiny": 10,
@@ -13,6 +13,7 @@ Download pipeline:
13
13
  Public API:
14
14
  resolve_and_install(force=False) -> InstallResult
15
15
  """
16
+ import re
16
17
  import shutil
17
18
  import subprocess
18
19
  import time
@@ -86,6 +87,54 @@ class InstallResult:
86
87
  return any(r.reboot_required for r in self.installed_ok)
87
88
 
88
89
 
90
+ def _parse_ver(s: str) -> tuple | None:
91
+ """Extract an x.x.x.x version tuple from a string (e.g. a pnputil 'Driver
92
+ Version: 12/02/2021 1.0.3.0' line or a repo version_label). None if absent."""
93
+ m = re.search(r"(\d+)\.(\d+)\.(\d+)\.(\d+)", s or "")
94
+ return tuple(int(x) for x in m.groups()) if m else None
95
+
96
+
97
+ def _installed_inf_index() -> dict:
98
+ """Map original INF name (lowercase) → highest installed driver-version tuple,
99
+ parsed from `pnputil /enum-drivers`. Lets us skip re-downloading/installing a
100
+ driver the machine already has at the same-or-newer version. Best-effort: on any
101
+ error/parse miss it returns {} so nothing is ever wrongly skipped."""
102
+ idx: dict = {}
103
+ try:
104
+ r = proc.run(["pnputil", "/enum-drivers"], capture_output=True, text=True, timeout=120)
105
+ if r.returncode != 0:
106
+ return {}
107
+ cur = None
108
+ for line in r.stdout.splitlines():
109
+ low = line.lower()
110
+ if "original name" in low and ":" in line:
111
+ cur = line.split(":", 1)[1].strip().lower()
112
+ elif "driver version" in low and cur:
113
+ v = _parse_ver(line)
114
+ if v and (cur not in idx or v > idx[cur]):
115
+ idx[cur] = v
116
+ cur = None
117
+ except Exception:
118
+ return {}
119
+ return idx
120
+
121
+
122
+ def _already_in_store(driver: dict, idx: dict) -> bool:
123
+ """True when every INF this driver ships is already in the driver store at a
124
+ same-or-newer version — so it needn't be downloaded/installed again. Conservative:
125
+ returns False (→ install) whenever the version or an INF can't be confirmed."""
126
+ if not idx:
127
+ return False
128
+ ver = _parse_ver(driver.get("version_label", ""))
129
+ if not ver:
130
+ return False
131
+ infs = [f["filename"].lower() for f in driver.get("files", [])
132
+ if f.get("file_role") == "inf" or f.get("filename", "").lower().endswith(".inf")]
133
+ if not infs:
134
+ return False
135
+ return all(idx.get(name) is not None and idx[name] >= ver for name in infs)
136
+
137
+
89
138
  def resolve_and_install(force: bool = False) -> InstallResult:
90
139
  """
91
140
  Run resolve (or load cache) then download + install all available drivers.
@@ -99,14 +148,38 @@ def resolve_and_install(force: bool = False) -> InstallResult:
99
148
  total=0)
100
149
  return InstallResult(trace_id=result.trace_id)
101
150
 
151
+ # Server now offers EVERY driver matching each device (primary + extensions +
152
+ # companions), so nothing is dropped by an assumption. Freshness is decided HERE
153
+ # against the real driver store: skip any package whose INF(s) are already present
154
+ # at the same-or-newer version — so a still-current big driver (graphics) is not
155
+ # re-downloaded, while missing/extension drivers still install. Toggle off with
156
+ # skip_installed_drivers=false to force-reinstall everything.
157
+ drivers = result.drivers
158
+ if cfg.get("skip_installed_drivers", True):
159
+ idx = _installed_inf_index()
160
+ kept = [d for d in drivers if not _already_in_store(d, idx)]
161
+ skipped = len(drivers) - len(kept)
162
+ if skipped:
163
+ events.progress(
164
+ "install",
165
+ f"[install] {skipped} driver(s) already current in the driver store — skipping",
166
+ skipped=skipped,
167
+ )
168
+ drivers = kept
169
+ if not drivers:
170
+ events.done("install",
171
+ "[install] Nothing to install — all resolved drivers already current",
172
+ total=0)
173
+ return InstallResult(trace_id=result.trace_id)
174
+
102
175
  session_id = str(uuid.uuid4())
103
176
  work_dir = Path(cfg.get("work_dir", "C:\\DriverServer\\client\\staging")) / session_id
104
177
  work_dir.mkdir(parents=True, exist_ok=True)
105
178
 
106
179
  events.start("install",
107
- f"[install] {len(result.drivers)} driver(s) — starting parallel downloads…",
108
- total=len(result.drivers))
109
- all_results = _stream_install(result.drivers, work_dir, cfg)
180
+ f"[install] {len(drivers)} driver(s) — starting parallel downloads…",
181
+ total=len(drivers))
182
+ all_results = _stream_install(drivers, work_dir, cfg)
110
183
 
111
184
  shutil.rmtree(work_dir, ignore_errors=True)
112
185
 
File without changes
File without changes