driverclient 0.2.32__tar.gz → 0.2.33__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 (25) hide show
  1. {driverclient-0.2.32 → driverclient-0.2.33}/PKG-INFO +32 -1
  2. {driverclient-0.2.32 → driverclient-0.2.33}/README.md +31 -0
  3. {driverclient-0.2.32 → driverclient-0.2.33}/pyproject.toml +1 -1
  4. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/__init__.py +1 -1
  5. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/config.py +7 -0
  6. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/core/http.py +68 -43
  7. {driverclient-0.2.32 → driverclient-0.2.33}/.gitignore +0 -0
  8. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/__main__.py +0 -0
  9. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/config.json +0 -0
  10. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/core/__init__.py +0 -0
  11. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/core/hardware.py +0 -0
  12. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/core/hwid.py +0 -0
  13. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/core/proc.py +0 -0
  14. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/events.py +0 -0
  15. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/main.py +0 -0
  16. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/ops/__init__.py +0 -0
  17. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/ops/automate.py +0 -0
  18. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/ops/capture.py +0 -0
  19. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/ops/diagnose.py +0 -0
  20. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/ops/download.py +0 -0
  21. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/ops/install.py +0 -0
  22. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/ops/offline.py +0 -0
  23. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/ops/resolve.py +0 -0
  24. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/ops/scan.py +0 -0
  25. {driverclient-0.2.32 → driverclient-0.2.33}/src/driverclient/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: driverclient
3
- Version: 0.2.32
3
+ Version: 0.2.33
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>
@@ -203,3 +203,34 @@ class Panel:
203
203
 
204
204
  - Python >= 3.10
205
205
  - Windows target for the actual driver operations (`pnputil`, WMI, DISM).
206
+
207
+ ## Building & releasing
208
+
209
+ The version lives in `pyproject.toml` **and** `src/driverclient/__init__.py` (kept
210
+ in lockstep by `bump_version.py`). System Assistant consumes this as a pip package
211
+ (`driverclient>=X` in its `requirements.txt`), so a code change ships by publishing
212
+ a **new version** — PyPI never overwrites an existing one.
213
+
214
+ ### With uv (no extra tooling to install)
215
+
216
+ `uv build` runs the hatchling backend in an isolated env; `uvx` runs `twine`
217
+ ephemerally — neither needs a pre-installed `build`/`twine`.
218
+
219
+ ```bash
220
+ cd client-package
221
+ uv build # → dist/driverclient-<version>-py3-none-any.whl (+ .tar.gz)
222
+ uvx twine upload dist/* # publish (uses ~/.pypirc)
223
+ ```
224
+
225
+ ### Bump vs. no-bump — pick the right one
226
+
227
+ - **Version NOT set yet** → bump then build:
228
+ `make release VERSION_PART=patch|minor|major` (bump → build → smoke, no upload),
229
+ then `twine upload dist/*` (or `uvx twine upload dist/*`).
230
+ - **Version already set to the exact target** (e.g. someone set it by hand): build
231
+ **without** bumping — `uv build` (or `make clean && make build`), then upload.
232
+ Do **not** run `make release`/`bump_version.py` here — it would bump *past* the
233
+ intended version.
234
+
235
+ Full flow (TestPyPI dry-run, `.pypirc` token, post-publish verify): see
236
+ [RELEASING.md](RELEASING.md).
@@ -187,3 +187,34 @@ class Panel:
187
187
 
188
188
  - Python >= 3.10
189
189
  - Windows target for the actual driver operations (`pnputil`, WMI, DISM).
190
+
191
+ ## Building & releasing
192
+
193
+ The version lives in `pyproject.toml` **and** `src/driverclient/__init__.py` (kept
194
+ in lockstep by `bump_version.py`). System Assistant consumes this as a pip package
195
+ (`driverclient>=X` in its `requirements.txt`), so a code change ships by publishing
196
+ a **new version** — PyPI never overwrites an existing one.
197
+
198
+ ### With uv (no extra tooling to install)
199
+
200
+ `uv build` runs the hatchling backend in an isolated env; `uvx` runs `twine`
201
+ ephemerally — neither needs a pre-installed `build`/`twine`.
202
+
203
+ ```bash
204
+ cd client-package
205
+ uv build # → dist/driverclient-<version>-py3-none-any.whl (+ .tar.gz)
206
+ uvx twine upload dist/* # publish (uses ~/.pypirc)
207
+ ```
208
+
209
+ ### Bump vs. no-bump — pick the right one
210
+
211
+ - **Version NOT set yet** → bump then build:
212
+ `make release VERSION_PART=patch|minor|major` (bump → build → smoke, no upload),
213
+ then `twine upload dist/*` (or `uvx twine upload dist/*`).
214
+ - **Version already set to the exact target** (e.g. someone set it by hand): build
215
+ **without** bumping — `uv build` (or `make clean && make build`), then upload.
216
+ Do **not** run `make release`/`bump_version.py` here — it would bump *past* the
217
+ intended version.
218
+
219
+ Full flow (TestPyPI dry-run, `.pypirc` token, post-publish verify): see
220
+ [RELEASING.md](RELEASING.md).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "driverclient"
7
- version = "0.2.32"
7
+ version = "0.2.33"
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"
@@ -26,7 +26,7 @@ from driverclient.main import run as _run
26
26
 
27
27
  __all__ = ["ClientEvent", "DriverClient"]
28
28
 
29
- __version__ = "0.2.32"
29
+ __version__ = "0.2.33"
30
30
 
31
31
 
32
32
  class DriverClient:
@@ -94,6 +94,13 @@ DEFAULTS: dict = {
94
94
  # driver (e.g. graphics) is never re-downloaded, while missing/extension drivers
95
95
  # still install. Set False to force-reinstall everything the server offers.
96
96
  "skip_installed_drivers": True,
97
+ # When the repo doesn't yet have a file (it returns HTTP 503 + Retry-After
98
+ # while it repairs/pulls that file from upstream), how long the client waits
99
+ # for the file to become available before giving up on that download. Bounds
100
+ # the per-file wait so a genuinely-absent file can't hang the pass. Raise it
101
+ # on slow inter-site links where a large driver (Intel/AMD GPU) is still
102
+ # syncing down; the client re-polls honoring the server's Retry-After.
103
+ "download_repair_wait_seconds": 300,
97
104
  # ── HTTP timeouts (seconds) ───────────────────────────────────────────
98
105
  "timeout_tiny": 10,
99
106
  "timeout_short": 30,
@@ -222,50 +222,75 @@ def http_download(url: str, dest: Path, sha256_expected: str, timeout: int | Non
222
222
  dest.parent.mkdir(parents=True, exist_ok=True)
223
223
 
224
224
  tmp = dest.with_suffix(dest.suffix + ".part")
225
- try:
226
- with urllib.request.urlopen(req, timeout=t) as resp:
227
- encoding = resp.headers.get("Content-Encoding", "")
228
- content_length = resp.headers.get("Content-Length")
229
- total = int(content_length) if content_length else None
230
-
231
- hasher = hashlib.sha256()
232
- received = 0
233
-
234
- if encoding == "zstd":
225
+ hasher = hashlib.sha256()
226
+ received = 0
227
+ # A missing file on the repo is NOT a hard failure: the server returns 503 +
228
+ # Retry-After while it repairs the file from upstream (files.py on-demand
229
+ # repair). Wait out the repair honoring Retry-After instead of the old
230
+ # fast 3-try give-up — otherwise a big driver still syncing down (e.g. the
231
+ # ~1.3 GB Intel GPU) fails to download and never installs/binds. Bounded by
232
+ # download_repair_wait_seconds so a genuinely-absent file can't hang forever.
233
+ repair_deadline = time.monotonic() + float(cfg.get("download_repair_wait_seconds", 300))
234
+ while True:
235
+ hasher = hashlib.sha256()
236
+ received = 0
237
+ try:
238
+ with urllib.request.urlopen(req, timeout=t) as resp:
239
+ encoding = resp.headers.get("Content-Encoding", "")
240
+ content_length = resp.headers.get("Content-Length")
241
+ total = int(content_length) if content_length else None
242
+
243
+ if encoding == "zstd":
244
+ try:
245
+ import zstandard as zstd
246
+ except ImportError:
247
+ print(
248
+ f" [ERROR] zstandard package required to decompress {dest.name} "
249
+ "— install it with: pip install zstandard"
250
+ )
251
+ return False
252
+
253
+ dctx = zstd.ZstdDecompressor()
254
+ with (
255
+ open(tmp, "wb") as fh,
256
+ dctx.stream_writer(fh, closefd=False) as writer,
257
+ ):
258
+ while chunk := resp.read(_CHUNK):
259
+ writer.write(chunk)
260
+ received += len(chunk)
261
+ _print_progress(dest.name, received, total)
262
+
263
+ with open(tmp, "rb") as rfh:
264
+ while chunk := rfh.read(_CHUNK):
265
+ hasher.update(chunk)
266
+ else:
267
+ with open(tmp, "wb") as fh:
268
+ while chunk := resp.read(_CHUNK):
269
+ fh.write(chunk)
270
+ hasher.update(chunk)
271
+ received += len(chunk)
272
+ _print_progress(dest.name, received, total)
273
+ break # streamed OK — fall through to hash verification
274
+ except urllib.error.HTTPError as e:
275
+ if e.code == 503 and time.monotonic() < repair_deadline:
235
276
  try:
236
- import zstandard as zstd
237
- except ImportError:
238
- print(
239
- f" [ERROR] zstandard package required to decompress {dest.name} "
240
- "— install it with: pip install zstandard"
241
- )
242
- return False
243
-
244
- dctx = zstd.ZstdDecompressor()
245
- with (
246
- open(tmp, "wb") as fh,
247
- dctx.stream_writer(fh, closefd=False) as writer,
248
- ):
249
- while chunk := resp.read(_CHUNK):
250
- writer.write(chunk)
251
- received += len(chunk)
252
- _print_progress(dest.name, received, total)
253
-
254
- with open(tmp, "rb") as rfh:
255
- while chunk := rfh.read(_CHUNK):
256
- hasher.update(chunk)
257
- else:
258
- with open(tmp, "wb") as fh:
259
- while chunk := resp.read(_CHUNK):
260
- fh.write(chunk)
261
- hasher.update(chunk)
262
- received += len(chunk)
263
- _print_progress(dest.name, received, total)
264
-
265
- except Exception as e:
266
- tmp.unlink(missing_ok=True)
267
- print(f" [ERROR] Download failed {dest.name}: {e}")
268
- return False
277
+ wait = float(e.headers.get("Retry-After", "15"))
278
+ except (TypeError, ValueError):
279
+ wait = 15.0
280
+ tmp.unlink(missing_ok=True)
281
+ print(
282
+ f" [repair] {dest.name} not on repo yet (503) — repo is fetching it "
283
+ f"from upstream, waiting {wait:.0f}s then retrying…"
284
+ )
285
+ time.sleep(wait)
286
+ continue
287
+ tmp.unlink(missing_ok=True)
288
+ print(f" [ERROR] Download failed {dest.name}: HTTP {e.code}")
289
+ return False
290
+ except Exception as e:
291
+ tmp.unlink(missing_ok=True)
292
+ print(f" [ERROR] Download failed {dest.name}: {e}")
293
+ return False
269
294
 
270
295
  actual = hasher.hexdigest()
271
296
  if actual != sha256_expected:
File without changes