programasweights 0.4.4__tar.gz → 0.4.5__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 (74) hide show
  1. programasweights-0.4.5/.github/workflows/test.yml +82 -0
  2. {programasweights-0.4.4 → programasweights-0.4.5}/CHANGELOG.md +14 -0
  3. {programasweights-0.4.4 → programasweights-0.4.5}/PKG-INFO +16 -3
  4. {programasweights-0.4.4 → programasweights-0.4.5}/PYPI_README.md +14 -1
  5. {programasweights-0.4.4 → programasweights-0.4.5}/README.md +14 -1
  6. {programasweights-0.4.4 → programasweights-0.4.5}/docs/api-reference/python-sdk.md +90 -5
  7. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/__init__.py +45 -22
  8. programasweights-0.4.5/programasweights/_program_reference.py +61 -0
  9. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/client.py +11 -7
  10. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/convert_peft_to_paw.py +12 -14
  11. programasweights-0.4.5/programasweights/errors.py +79 -0
  12. programasweights-0.4.5/programasweights/local_program.py +196 -0
  13. {programasweights-0.4.4 → programasweights-0.4.5}/pyproject.toml +1 -1
  14. programasweights-0.4.5/tests/test_api_errors.py +222 -0
  15. programasweights-0.4.5/tests/test_compile_timeouts.py +129 -0
  16. programasweights-0.4.5/tests/test_local_program.py +676 -0
  17. programasweights-0.4.4/.github/workflows/test.yml +0 -43
  18. {programasweights-0.4.4 → programasweights-0.4.5}/.gitignore +0 -0
  19. {programasweights-0.4.4 → programasweights-0.4.5}/.readthedocs.yaml +0 -0
  20. {programasweights-0.4.4 → programasweights-0.4.5}/AGENTS.md +0 -0
  21. {programasweights-0.4.4 → programasweights-0.4.5}/LICENSE +0 -0
  22. {programasweights-0.4.4 → programasweights-0.4.5}/docs/adr/001-llama-cpp-over-pytorch.md +0 -0
  23. {programasweights-0.4.4 → programasweights-0.4.5}/docs/adr/002-q4_0-adapter-format.md +0 -0
  24. {programasweights-0.4.4 → programasweights-0.4.5}/docs/adr/003-single-spec-field.md +0 -0
  25. {programasweights-0.4.4 → programasweights-0.4.5}/docs/adr/004-compiler-naming.md +0 -0
  26. {programasweights-0.4.4 → programasweights-0.4.5}/docs/adr/005-vllm-hidden-states.md +0 -0
  27. {programasweights-0.4.4 → programasweights-0.4.5}/docs/adr/006-email-api-key-auth.md +0 -0
  28. {programasweights-0.4.4 → programasweights-0.4.5}/docs/advanced/adrs.md +0 -0
  29. {programasweights-0.4.4 → programasweights-0.4.5}/docs/advanced/architecture.md +0 -0
  30. {programasweights-0.4.4 → programasweights-0.4.5}/docs/api-reference/cli.md +0 -0
  31. {programasweights-0.4.4 → programasweights-0.4.5}/docs/api-reference/rest-api.md +0 -0
  32. {programasweights-0.4.4 → programasweights-0.4.5}/docs/architecture.md +0 -0
  33. {programasweights-0.4.4 → programasweights-0.4.5}/docs/case-studies/alien-taboo.md +0 -0
  34. {programasweights-0.4.4 → programasweights-0.4.5}/docs/case-studies/log-monitoring.md +0 -0
  35. {programasweights-0.4.4 → programasweights-0.4.5}/docs/case-studies/semantic-search.md +0 -0
  36. {programasweights-0.4.4 → programasweights-0.4.5}/docs/case-studies/site-navigation.md +0 -0
  37. {programasweights-0.4.4 → programasweights-0.4.5}/docs/case-studies/tool-calling.md +0 -0
  38. {programasweights-0.4.4 → programasweights-0.4.5}/docs/getting-started/first-program.md +0 -0
  39. {programasweights-0.4.4 → programasweights-0.4.5}/docs/getting-started/installation.md +0 -0
  40. {programasweights-0.4.4 → programasweights-0.4.5}/docs/getting-started/naming-programs.md +0 -0
  41. {programasweights-0.4.4 → programasweights-0.4.5}/docs/guide/browser-inference.md +0 -0
  42. {programasweights-0.4.4 → programasweights-0.4.5}/docs/guide/how-it-works.md +0 -0
  43. {programasweights-0.4.4 → programasweights-0.4.5}/docs/guide/local-inference.md +0 -0
  44. {programasweights-0.4.4 → programasweights-0.4.5}/docs/guide/writing-good-specs.md +0 -0
  45. {programasweights-0.4.4 → programasweights-0.4.5}/docs/hub/browsing-programs.md +0 -0
  46. {programasweights-0.4.4 → programasweights-0.4.5}/docs/hub/feedback-cases.md +0 -0
  47. {programasweights-0.4.4 → programasweights-0.4.5}/docs/hub/publishing-programs.md +0 -0
  48. {programasweights-0.4.4 → programasweights-0.4.5}/docs/index.md +0 -0
  49. {programasweights-0.4.4 → programasweights-0.4.5}/docs/requirements.txt +0 -0
  50. {programasweights-0.4.4 → programasweights-0.4.5}/examples/flask_app.py +0 -0
  51. {programasweights-0.4.4 → programasweights-0.4.5}/examples/jupyter_notebook.py +0 -0
  52. {programasweights-0.4.4 → programasweights-0.4.5}/examples/langchain_integration.py +0 -0
  53. {programasweights-0.4.4 → programasweights-0.4.5}/examples/paw_monitor.py +0 -0
  54. {programasweights-0.4.4 → programasweights-0.4.5}/examples/replace_openai.py +0 -0
  55. {programasweights-0.4.4 → programasweights-0.4.5}/mkdocs.yml +0 -0
  56. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/_output.py +0 -0
  57. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/artifacts.py +0 -0
  58. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/cache.py +0 -0
  59. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/cli.py +0 -0
  60. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/compiler/__init__.py +0 -0
  61. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/compiler/dummy.py +0 -0
  62. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/config.py +0 -0
  63. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/paw_format.py +0 -0
  64. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/runtime/__init__.py +0 -0
  65. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/runtime/interpreter.py +0 -0
  66. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/runtime/interpreter_onnx.py +0 -0
  67. {programasweights-0.4.4 → programasweights-0.4.5}/programasweights/runtime_llamacpp.py +0 -0
  68. {programasweights-0.4.4 → programasweights-0.4.5}/tests/test_base_interpreter.py +0 -0
  69. {programasweights-0.4.4 → programasweights-0.4.5}/tests/test_cli_auth.py +0 -0
  70. {programasweights-0.4.4 → programasweights-0.4.5}/tests/test_desktop_sdk.py +0 -0
  71. {programasweights-0.4.4 → programasweights-0.4.5}/tests/test_offline_cache.py +0 -0
  72. {programasweights-0.4.4 → programasweights-0.4.5}/tests/test_runtime_registry_sdk.py +0 -0
  73. {programasweights-0.4.4 → programasweights-0.4.5}/tests/test_sdk.py +0 -0
  74. {programasweights-0.4.4 → programasweights-0.4.5}/tests/test_sdk.sh +0 -0
@@ -0,0 +1,82 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Set up Python ${{ matrix.python-version }}
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install (hermetic deps only)
24
+ # Install httpx + pytest and the package itself without pulling the heavy
25
+ # llama-cpp-python build. Runtime tests inject a fake llama_cpp module,
26
+ # so CI needs neither the native extension nor a model download.
27
+ run: |
28
+ python -m pip install --upgrade pip
29
+ python -m pip install httpx pytest
30
+ python -m pip install -e . --no-deps
31
+
32
+ - name: Run hermetic tests
33
+ # Scoped to tests that need no network, no model download, and no
34
+ # PAW_API_KEY. Auth tests (@needs_auth) auto-skip without a key; the
35
+ # network/model-download tests in test_sdk.py are excluded here and can
36
+ # be run separately against a live server.
37
+ run: |
38
+ pytest \
39
+ tests/test_api_errors.py \
40
+ tests/test_compile_timeouts.py \
41
+ tests/test_local_program.py \
42
+ tests/test_base_interpreter.py \
43
+ tests/test_cli_auth.py \
44
+ tests/test_desktop_sdk.py \
45
+ tests/test_runtime_registry_sdk.py \
46
+ tests/test_sdk.py::TestInstallAndImport
47
+
48
+ local-files-windows:
49
+ runs-on: windows-latest
50
+ strategy:
51
+ fail-fast: false
52
+ matrix:
53
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
54
+ steps:
55
+ - uses: actions/checkout@v4
56
+ - uses: actions/setup-python@v5
57
+ with:
58
+ python-version: ${{ matrix.python-version }}
59
+ - name: Install hermetic test dependencies
60
+ run: |
61
+ python -m pip install httpx pytest
62
+ python -m pip install -e . --no-deps
63
+ - name: Test Windows local paths, cache locks, and compile errors
64
+ run: python -m pytest tests/test_api_errors.py tests/test_compile_timeouts.py tests/test_local_program.py --junitxml=test-results.xml
65
+ - name: Annotate Windows test failures
66
+ if: failure()
67
+ shell: python
68
+ run: |
69
+ from pathlib import Path
70
+ import xml.etree.ElementTree as ET
71
+
72
+ report = Path("test-results.xml")
73
+ if report.exists():
74
+ for case in ET.parse(report).iter("testcase"):
75
+ for result in case:
76
+ if result.tag in {"failure", "error"}:
77
+ # GitHub truncates annotations, so retain the actual
78
+ # exception at the end of long pytest tracebacks.
79
+ detail = result.text or result.get("message", "")
80
+ message = f"{case.get('name')}: {detail[-3500:]}"
81
+ message = message.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
82
+ print(f"::error::{message}")
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.5 (2026-09-10)
4
+
5
+ - Expose structured compile API failures as `paw.APIError`, compatible with
6
+ `httpx.HTTPStatusError`, preserving server code, message, request ID, and the
7
+ original response. Transport errors propagate unchanged; no automatic retries.
8
+ - Allow synchronous compilation a 2,400-second read timeout while keeping
9
+ connect, write, and pool timeouts at 120 seconds. Async submission stays at
10
+ 30 seconds; precheck, status, and cancellation stay at 10 seconds.
11
+ - Load current GGUF ZIP `.paw` files through `paw.function` using explicit
12
+ local paths or `Path` objects. Validated imports use a separate SHA-256 cache
13
+ without replacing Hub caches or source files; invalid local files never fall
14
+ back to Hub lookup. Shared base-model assets may still need downloading unless
15
+ offline mode is requested. Legacy tensor-format `.paw` files are unsupported.
16
+
3
17
  ## 0.4.4 (2026-07-18)
4
18
 
5
19
  - Add desktop preparation and cache inspection APIs with structured progress:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: programasweights
3
- Version: 0.4.4
3
+ Version: 0.4.5
4
4
  Summary: Compile natural language specifications into neural programs that run locally via llama.cpp.
5
5
  Project-URL: Homepage, https://programasweights.com
6
6
  Project-URL: Repository, https://github.com/programasweights/programasweights-python
@@ -102,6 +102,19 @@ finetune compiles can be queued with
102
102
  `paw.compile_async(spec, compiler="paw-ft-bs48")`; an explicit finetune
103
103
  compiler is required.
104
104
 
105
+ Load a saved current GGUF ZIP `.paw` bundle directly (SDK 0.4.5+):
106
+
107
+ ```python
108
+ from pathlib import Path
109
+ fn = paw.function(Path("classifier.paw"))
110
+ ```
111
+
112
+ Local files are validated into a separate SHA-256 cache without changing the
113
+ source or falling back to Hub lookup. Runtime metadata or the shared base model
114
+ may still download; `offline=True` prohibits those requests. Legacy tensor-format
115
+ `.paw` files are unsupported. Local-file inputs are supported by `function`,
116
+ not `prepare_program` or `is_offline_ready`.
117
+
105
118
  Advanced adapter-free inference is available with
106
119
  `paw.function(None, interpreter="gpt2")`; see the Python API reference for
107
120
  its intentionally strict semantics.
@@ -179,4 +192,4 @@ paw login
179
192
 
180
193
  ## License
181
194
 
182
- MIT
195
+ MIT
@@ -71,6 +71,19 @@ finetune compiles can be queued with
71
71
  `paw.compile_async(spec, compiler="paw-ft-bs48")`; an explicit finetune
72
72
  compiler is required.
73
73
 
74
+ Load a saved current GGUF ZIP `.paw` bundle directly (SDK 0.4.5+):
75
+
76
+ ```python
77
+ from pathlib import Path
78
+ fn = paw.function(Path("classifier.paw"))
79
+ ```
80
+
81
+ Local files are validated into a separate SHA-256 cache without changing the
82
+ source or falling back to Hub lookup. Runtime metadata or the shared base model
83
+ may still download; `offline=True` prohibits those requests. Legacy tensor-format
84
+ `.paw` files are unsupported. Local-file inputs are supported by `function`,
85
+ not `prepare_program` or `is_offline_ready`.
86
+
74
87
  Advanced adapter-free inference is available with
75
88
  `paw.function(None, interpreter="gpt2")`; see the Python API reference for
76
89
  its intentionally strict semantics.
@@ -148,4 +161,4 @@ paw login
148
161
 
149
162
  ## License
150
163
 
151
- MIT
164
+ MIT
@@ -71,6 +71,19 @@ finetune compiles can be queued with
71
71
  `paw.compile_async(spec, compiler="paw-ft-bs48")`; an explicit finetune
72
72
  compiler is required.
73
73
 
74
+ Load a saved current GGUF ZIP `.paw` bundle directly (SDK 0.4.5+):
75
+
76
+ ```python
77
+ from pathlib import Path
78
+ fn = paw.function(Path("classifier.paw"))
79
+ ```
80
+
81
+ Local files are validated into a separate SHA-256 cache without changing the
82
+ source or falling back to Hub lookup. Runtime metadata or the shared base model
83
+ may still download; `offline=True` prohibits those requests. Legacy tensor-format
84
+ `.paw` files are unsupported. Local-file inputs are supported by `function`,
85
+ not `prepare_program` or `is_offline_ready`.
86
+
74
87
  Advanced adapter-free inference is available with
75
88
  `paw.function(None, interpreter="gpt2")`; see the
76
89
  [Python API reference](docs/api-reference/python-sdk.md#advanced-adapter-free-base-interpreter)
@@ -149,4 +162,4 @@ paw login
149
162
 
150
163
  ## License
151
164
 
152
- MIT
165
+ MIT
@@ -28,15 +28,17 @@ fn = paw.function(
28
28
  )
29
29
  ```
30
30
 
31
- Loads a compiled program and returns a callable. Downloads the program and base model on first use; cached locally after that. Works offline after first download.
31
+ Loads a compiled program and returns a callable. Hub references download the
32
+ program and base model on first use; local `.paw` files supply the program
33
+ bundle directly. Required runtime metadata and base models are cached for reuse.
32
34
 
33
35
  | Parameter | Description |
34
36
  |-----------|-------------|
35
- | `program_id` | Required. A `Program` object, hash ID (e.g. `a6b454023d41ac9ca845`), slug (e.g. `da03/my-classifier`), or official shorthand (e.g. `email-triage`). A `Program` resolves by immutable `id`, not its mutable slug. |
37
+ | `program_id` | Required. A `Program` object, hash ID (e.g. `a6b454023d41ac9ca845`), slug (e.g. `da03/my-classifier`), official shorthand (e.g. `email-triage`), or local `.paw` path (see below). A `Program` resolves by immutable `id`, not its mutable slug. |
36
38
  | `n_ctx` | Context length for the local runtime (default `2048`). |
37
39
  | `n_gpu_layers` | GPU layers to offload (`0` = CPU-only, `-1` = all). The default is `-1`, or `PAW_GPU_LAYERS` when set. |
38
40
  | `verbose` | Enable verbose logging (default `False`). |
39
- | `offline` | Require all program/runtime/model assets to already be cached and make zero network calls. `PAW_OFFLINE=1` has the same effect. |
41
+ | `offline` | Use only local files/cache and make zero network calls; fail if required validated assets are missing. `PAW_OFFLINE=1` has the same effect. |
40
42
  | `interpreter` | Advanced adapter-free mode only. Must be passed by keyword and only when `program_id` is explicitly `None`. Supported values are `Qwen/Qwen3-0.6B` and `gpt2`. |
41
43
 
42
44
  The returned callable:
@@ -54,7 +56,7 @@ output: str = fn(input_text, max_tokens=None, temperature=0.0)
54
56
  **Context limits:** Spec + input + output share a ~2048 token window. Inputs that exceed it will error. `max_tokens` defaults to `None`: generation runs until EOS or the context limit.
55
57
 
56
58
  Compiled mode is strict: the adapter, prompt template, matching metadata,
57
- runtime manifest, and runtime-compatible base-model file must all validate. Version 0.4.4
59
+ runtime manifest, and runtime-compatible base-model file must all validate. Version 0.4.5
58
60
  accepts runtime manifest version 1 with `adapter_format="gguf_lora"`.
59
61
  Built-in models are checked against pinned size/SHA-256 metadata and GGUF
60
62
  magic. Historical manifests for those known runtime IDs are normalized to the
@@ -62,6 +64,45 @@ same canonical integrity metadata, so missing server-side checksum fields
62
64
  cannot weaken validation. Missing or failed adapters raise an error; the SDK
63
65
  never silently falls back to an unadapted base model.
64
66
 
67
+ ### Loading a local `.paw` file
68
+
69
+ Version 0.4.5 adds local-file inputs to `paw.function`:
70
+
71
+ ```python
72
+ from pathlib import Path
73
+
74
+ fn = paw.function(Path("classifier.paw"))
75
+ # With the required runtime metadata and base model already available locally:
76
+ fn = paw.function("./classifier.paw", offline=True)
77
+ ```
78
+
79
+ Use a current GGUF ZIP `.paw` bundle, such as one downloaded from a hosted
80
+ compile. It must contain `meta.json`, `adapter.gguf`, and `prompt_template.txt`,
81
+ with only `pseudo_program.txt` allowed as an optional extra; serialized native
82
+ prefix state is not accepted from archives. Local inputs are selected
83
+ deterministically: a `Path`/`os.PathLike`
84
+ object, an explicit path such as `./classifier.paw` or an absolute path, or a
85
+ string ending in `.paw` (case-insensitive). Ordinary IDs and slugs such as
86
+ `da03/my-classifier` keep their existing Hub behavior even if a matching local
87
+ file exists. Use `Path(...)` or an explicit path for a filename without the
88
+ `.paw` suffix. URL inputs are unsupported.
89
+
90
+ The bundle is validated and imported under
91
+ `PAW_CACHE_DIR/local_programs/<archive-sha256>` (default cache root:
92
+ `~/.cache/programasweights`). Its source file is unchanged, and its metadata
93
+ cannot replace a Hub program-ID or slug cache. Missing or invalid local files
94
+ raise an error without falling back to a Hub lookup or program download.
95
+
96
+ A local program does not necessarily make the first load fully offline:
97
+ the existing runtime policy may fetch required runtime metadata from PAW and
98
+ download the shared base model. Pass `offline=True` or set `PAW_OFFLINE=1`
99
+ to prohibit all network access. Historical `PAW\x02` tensor containers,
100
+ including output from the legacy `convert_peft_to_paw` module, are not supported
101
+ by this loader; it does not convert PEFT tensors to GGUF.
102
+
103
+ Only `paw.function` gains local-file inputs. `prepare_program` and
104
+ `is_offline_ready` continue to accept Hub program references.
105
+
65
106
  ### Advanced: adapter-free base interpreter
66
107
 
67
108
  Pass explicit `None` plus an interpreter to run the supported base GGUF without a compiled PAW program:
@@ -155,11 +196,24 @@ Compiles a natural language spec on the server. Returns a `Program` object.
155
196
  |-----------|-------------|
156
197
  | `id` | Hash-based program identifier. Use with `paw.function(program.id)`. |
157
198
  | `slug` | Full slug handle (e.g. `da03/my-classifier`) if one was created, `None` otherwise. |
158
- | `status` | `"ready"` on success, `"failed"` on error. |
199
+ | `status` | Status returned by the server, normally `"ready"` on success. HTTP errors raise `APIError` instead of returning a failed `Program`. |
159
200
  | `compiler_snapshot` | Exact compiler version used. |
160
201
  | `timings` | Timing metadata from the server. |
161
202
  | `error` | Error message when compilation fails. |
162
203
 
204
+ ### Compile timeouts
205
+
206
+ Synchronous `compile` uses `httpx.Timeout(120.0, read=2400.0)`: connect, write,
207
+ and pool waits remain 120 seconds; the read timeout is 2,400 seconds. This
208
+ allows for the origin's 1,900-second provider wait plus up to 330 seconds of
209
+ artifact finalization. It is a timeout while waiting for response data, **not a
210
+ 40-minute total deadline or guarantee**; upstream services may fail earlier.
211
+ The same setting applies to the compile step of `compile_and_load`.
212
+
213
+ Async submission retains a 30-second timeout. Precheck, status polling, and
214
+ cancellation each retain a 10-second timeout. For long finetunes, prefer the
215
+ explicit async workflow below so you retain a job ID for later status checks.
216
+
163
217
  ## Long-running compile jobs
164
218
 
165
219
  The asynchronous compile endpoint is available through both `PAWClient` and top-level helpers:
@@ -183,6 +237,37 @@ Status and cancellation requests must use the same authenticated account as
183
237
  submission. Anonymous jobs are bound to the validated client IP that submitted
184
238
  them.
185
239
 
240
+ ### Compile API errors
241
+
242
+ `compile`, `precheck_compile`, `compile_async`, `get_compile_status`, and
243
+ `cancel_compile` raise `paw.APIError` for HTTP 4xx/5xx responses. It is a subclass
244
+ of `httpx.HTTPStatusError`, so existing handlers continue to work. When supplied
245
+ by the server, `code`, `message`, and `request_id` are available as attributes
246
+ and included in the exception text. Missing fields are `None`; the original
247
+ `request` and `response` remain available, including response headers and body.
248
+
249
+ ```python
250
+ try:
251
+ job = paw.compile_async(SPEC, compiler="paw-ft-bs48")
252
+ except paw.APIError as error:
253
+ print(error.code, error.message, error.request_id)
254
+ # error.response.status_code and error.response.headers are unchanged.
255
+ raise
256
+ ```
257
+
258
+ For example, a `durable_queue_unavailable` 503 reports that durable Redis must
259
+ be healthy before async compilation can proceed. That rejection occurs before
260
+ the job is accepted; the caller can submit again after service recovery.
261
+ The SDK does not automatically retry compilation requests: other failures may
262
+ occur after a job has already been recorded. Invalid/non-JSON error bodies
263
+ retain the ordinary HTTP error description rather than displaying raw content.
264
+
265
+ Transport errors such as `httpx.ReadTimeout` propagate unchanged, rather than
266
+ becoming `APIError`. A timeout does not prove the server rejected or cancelled
267
+ the work, so the SDK does not automatically resubmit it. Both `paw.compile`
268
+ and `paw.compile_and_load` propagate these errors; `compile_and_load` does not
269
+ attempt to load a function when compilation raises.
270
+
186
271
  ## `paw.compile_and_load`
187
272
 
188
273
  ```python
@@ -27,7 +27,7 @@ try:
27
27
  from importlib.metadata import version as _meta_version
28
28
  __version__ = _meta_version("programasweights")
29
29
  except Exception:
30
- __version__ = "0.4.4"
30
+ __version__ = "0.4.5"
31
31
 
32
32
  from ._output import ProgressCallback, ProgressEvent, report_progress
33
33
  from .cache import CachedProgram
@@ -39,6 +39,7 @@ from .client import (
39
39
  Program,
40
40
  )
41
41
  from .config import get_api_url, get_api_key, set_api_key
42
+ from .errors import APIError
42
43
 
43
44
 
44
45
  def compile(
@@ -400,18 +401,22 @@ def function(
400
401
  ):
401
402
  """Load a compiled program, or explicitly load a bare base interpreter.
402
403
 
403
- Downloads the .paw bundle and base model GGUF on first use.
404
- Subsequent calls use the local cache.
404
+ Hub references download the .paw bundle on first use; local paths supply
405
+ it directly. Required runtime metadata and base models may still download
406
+ unless offline mode is enabled. Subsequent calls reuse validated caches.
405
407
 
406
408
  Args:
407
409
  program_id: Program ID (str), slug (``da03/my-program``), pinned version
408
- (``da03/my-program@v3``), or a ``Program`` object from compile().
410
+ (``da03/my-program@v3``), a ``Program`` object from compile(), or a
411
+ local GGUF-based .paw bundle. PathLike objects and explicit path
412
+ strings (including strings ending in .paw) select local files.
409
413
  n_ctx: Context window size for llama.cpp.
410
414
  n_gpu_layers: GPU layers (-1 = all GPU, 0 = CPU only). Defaults to -1
411
415
  (auto-uses Metal/CUDA if available, safe fallback to CPU).
412
416
  Set ``PAW_GPU_LAYERS=0`` env var to force CPU-only.
413
417
  verbose: Print llama.cpp debug output.
414
- offline: Skip server check for slug resolution and use local cache only.
418
+ offline: Prohibit network access. Local bundles may be imported, but
419
+ their runtime and base model must already be available locally.
415
420
  Also set via ``PAW_OFFLINE=1`` env var.
416
421
  interpreter: Advanced adapter-free mode. This is only valid when
417
422
  ``program_id`` is explicitly ``None``. Initially supported values
@@ -427,6 +432,8 @@ def function(
427
432
 
428
433
  >>> fn = paw.function("da03/my-program@v2") # pinned version
429
434
 
435
+ >>> fn = paw.function("./classifier.paw") # local bundle
436
+
430
437
  >>> base = paw.function(None, interpreter="gpt2")
431
438
  """
432
439
  import os
@@ -452,7 +459,12 @@ def function(
452
459
  offline=offline,
453
460
  )
454
461
 
455
- program_reference = _coerce_program_reference(program_id)
462
+ from ._program_reference import local_program_path
463
+
464
+ local_path = local_program_path(program_id)
465
+ program_reference = (
466
+ _coerce_program_reference(program_id) if local_path is None else None
467
+ )
456
468
  if program_reference == "":
457
469
  raise ValueError(
458
470
  "program_id cannot be an empty string; pass explicit None with "
@@ -464,25 +476,35 @@ def function(
464
476
  "program_id=None to request adapter-free base mode."
465
477
  )
466
478
 
467
- from .runtime_llamacpp import PawFunction
479
+ if local_path is not None:
480
+ from .local_program import import_local_program
468
481
 
469
- resolved_id = _resolve_program_id(program_reference, offline=offline)
470
- if offline and not cache.has_valid_program_assets(resolved_id):
471
- raise RuntimeError(
472
- f"Program {resolved_id} is not fully cached; offline mode "
473
- "prohibits program downloads."
474
- )
475
- if not cache.has_valid_program_assets(resolved_id):
476
- from .client import PAWClient
482
+ # Validate explicit local input before importing the native runtime.
483
+ # A missing/corrupt file is never retried as a Hub ID or slug.
484
+ program_dir = import_local_program(local_path)
485
+ from .runtime_llamacpp import PawFunction
486
+ else:
487
+ # Preserve the existing Hub path's fail-fast dependency check before
488
+ # resolving slugs or downloading assets.
489
+ from .runtime_llamacpp import PawFunction
477
490
 
478
- client = PAWClient(api_url=get_api_url(), api_key=get_api_key())
479
- client.download_paw(resolved_id)
480
- if not cache.has_valid_program_assets(resolved_id):
481
- raise RuntimeError(
482
- f"Program {resolved_id} is missing valid compiled assets."
483
- )
491
+ resolved_id = _resolve_program_id(program_reference, offline=offline)
492
+ if offline and not cache.has_valid_program_assets(resolved_id):
493
+ raise RuntimeError(
494
+ f"Program {resolved_id} is not fully cached; offline mode "
495
+ "prohibits program downloads."
496
+ )
497
+ if not cache.has_valid_program_assets(resolved_id):
498
+ from .client import PAWClient
499
+
500
+ client = PAWClient(api_url=get_api_url(), api_key=get_api_key())
501
+ client.download_paw(resolved_id)
502
+ if not cache.has_valid_program_assets(resolved_id):
503
+ raise RuntimeError(
504
+ f"Program {resolved_id} is missing valid compiled assets."
505
+ )
506
+ program_dir = cache.get_program_dir(resolved_id)
484
507
 
485
- program_dir = cache.get_program_dir(resolved_id)
486
508
  return PawFunction(
487
509
  program_dir,
488
510
  n_ctx=n_ctx,
@@ -616,6 +638,7 @@ def list_compilers() -> list[dict]:
616
638
 
617
639
 
618
640
  __all__ = [
641
+ "APIError",
619
642
  "CachedProgram",
620
643
  "CompileCancellation",
621
644
  "CompileJob",
@@ -0,0 +1,61 @@
1
+ """Deterministic local-file intent, independent of filesystem contents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+
9
+
10
+ _WINDOWS_DRIVE = re.compile(r"^[A-Za-z]:")
11
+ _URI_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
12
+
13
+
14
+ def local_program_path(reference: object) -> Path | None:
15
+ """Return an explicit local path, or None for an ID/slug/Program.
16
+
17
+ Path-like objects always mean files. Strings select files by syntax, never
18
+ by existence; in particular, an ``owner/slug`` remains a Hub reference.
19
+ """
20
+ is_pathlike = isinstance(reference, os.PathLike)
21
+ if is_pathlike:
22
+ value = os.fspath(reference)
23
+ if not isinstance(value, str):
24
+ raise TypeError("Local program paths must be text, not bytes.")
25
+ elif isinstance(reference, str):
26
+ value = reference
27
+ else:
28
+ return None
29
+
30
+ if "\x00" in value:
31
+ raise ValueError("Program references cannot contain null bytes.")
32
+ windows_drive = bool(_WINDOWS_DRIVE.match(value))
33
+ if not is_pathlike and not windows_drive and _URI_SCHEME.match(value):
34
+ raise ValueError(
35
+ "Program URLs are not supported. Download the .paw bundle first "
36
+ "and pass its local path."
37
+ )
38
+
39
+ explicit_path = value.startswith(
40
+ ("./", "../", "/", "~/", "\\", ".\\", "..\\", "~\\")
41
+ )
42
+ if not (
43
+ is_pathlike or windows_drive or explicit_path
44
+ or value.lower().endswith(".paw")
45
+ ):
46
+ return None
47
+ if not value:
48
+ raise ValueError("Local program path cannot be empty.")
49
+
50
+ # A Windows-looking string must never be sent to the Hub on another OS.
51
+ # Path objects, however, explicitly name a native path and may contain
52
+ # characters that would have a different meaning on another platform.
53
+ windows_path = windows_drive or value.startswith(
54
+ ("\\", ".\\", "..\\", "~\\")
55
+ )
56
+ if not is_pathlike and os.name != "nt" and windows_path:
57
+ raise ValueError(
58
+ "This is a Windows filesystem path, which cannot be opened on "
59
+ "this platform. Pass a native local path instead."
60
+ )
61
+ return Path(value).expanduser()
@@ -21,6 +21,7 @@ import httpx
21
21
 
22
22
  from . import config
23
23
  from ._output import ProgressCallback, report_progress
24
+ from .errors import raise_for_api_status
24
25
 
25
26
  MAX_PAW_ARCHIVE_BYTES = 256 * 1024 * 1024
26
27
  MAX_PAW_ARCHIVE_MEMBERS = 256
@@ -170,7 +171,7 @@ class PAWClient:
170
171
  Program with id, slug, status, and timings.
171
172
 
172
173
  Raises:
173
- httpx.HTTPStatusError: On API errors (422 for validation, 429 for rate limit).
174
+ APIError: On API errors; also caught by httpx.HTTPStatusError.
174
175
  """
175
176
  body = self._compile_body(
176
177
  spec,
@@ -186,9 +187,12 @@ class PAWClient:
186
187
  f"{self._api_url}/api/v1/compile",
187
188
  json=body,
188
189
  headers=self._headers(),
189
- timeout=120.0,
190
+ # Allow a long response wait (1900s provider + 330s finalization)
191
+ # without extending connect/write/pool timeouts beyond 120s.
192
+ # This is a read timeout, not a total deadline; prefer compile_async().
193
+ timeout=httpx.Timeout(120.0, read=2400.0),
190
194
  )
191
- resp.raise_for_status()
195
+ raise_for_api_status(resp)
192
196
  data = resp.json()
193
197
 
194
198
  return Program(
@@ -221,7 +225,7 @@ class PAWClient:
221
225
  headers=self._headers(),
222
226
  timeout=10.0,
223
227
  )
224
- resp.raise_for_status()
228
+ raise_for_api_status(resp)
225
229
  return cast(CompilePrecheck, resp.json())
226
230
 
227
231
  def compile_async(
@@ -260,7 +264,7 @@ class PAWClient:
260
264
  headers=self._headers(),
261
265
  timeout=30.0,
262
266
  )
263
- resp.raise_for_status()
267
+ raise_for_api_status(resp)
264
268
  return cast(CompileJob, resp.json())
265
269
 
266
270
  def get_compile_status(self, job_id: str) -> CompileStatus:
@@ -270,7 +274,7 @@ class PAWClient:
270
274
  headers=self._headers(),
271
275
  timeout=10.0,
272
276
  )
273
- resp.raise_for_status()
277
+ raise_for_api_status(resp)
274
278
  return cast(CompileStatus, resp.json())
275
279
 
276
280
  def cancel_compile(self, job_id: str) -> CompileCancellation:
@@ -283,7 +287,7 @@ class PAWClient:
283
287
  headers=self._headers(),
284
288
  timeout=10.0,
285
289
  )
286
- resp.raise_for_status()
290
+ raise_for_api_status(resp)
287
291
  return cast(CompileCancellation, resp.json())
288
292
 
289
293
  def resolve_slug(self, slug: str) -> str:
@@ -1,15 +1,11 @@
1
1
  """
2
- Convert a PEFT/LoRA checkpoint to .paw format.
2
+ Legacy PEFT/LoRA converter for the PAW-v2 tensor format.
3
3
 
4
- Supports the standard PEFT workflow:
5
- 1. Train with HuggingFace PEFT library
6
- 2. Save adapter: model.save_pretrained("my_adapter/")
7
- 3. Convert to .paw: paw.from_peft("my_adapter/", "output.paw", spec="...")
8
-
9
- The resulting .paw file can be:
10
- - Shared on the Program Hub
11
- - Loaded with paw.function("output.paw")
12
- - Distributed as a single file
4
+ This module writes the historical PAW\\x02 tensor container, not the current
5
+ GGUF ZIP .paw bundle. Its output is NOT supported by the modern public
6
+ paw.function local-file loader. It does not convert PEFT weights to GGUF, and
7
+ from_peft is not exported as paw.from_peft. Use a current hosted-compile bundle
8
+ for the public loader; this legacy converter is retained for older workflows.
13
9
  """
14
10
 
15
11
  import json
@@ -34,7 +30,9 @@ def from_peft(
34
30
  interpreter_model: Optional[str] = None,
35
31
  ) -> str:
36
32
  """
37
- Convert a PEFT/LoRA adapter to .paw format.
33
+ Convert a PEFT/LoRA adapter to the legacy, non-GGUF .paw tensor format.
34
+
35
+ The output cannot be loaded by the modern public paw.function API.
38
36
 
39
37
  Args:
40
38
  adapter_path: Path to PEFT adapter directory (contains adapter_config.json + adapter_model.safetensors)
@@ -51,7 +49,7 @@ def from_peft(
51
49
  Path to saved .paw file
52
50
 
53
51
  Example:
54
- >>> import programasweights as paw
52
+ >>> from programasweights.convert_peft_to_paw import from_peft
55
53
  >>>
56
54
  >>> # After training with PEFT:
57
55
  >>> # from peft import get_peft_model, LoraConfig
@@ -59,8 +57,8 @@ def from_peft(
59
57
  >>> # trainer.train()
60
58
  >>> # model.save_pretrained("my_adapter/")
61
59
  >>>
62
- >>> # Convert to .paw:
63
- >>> paw.from_peft(
60
+ >>> # Convert for a legacy tensor-format consumer, not paw.function:
61
+ >>> from_peft(
64
62
  ... "my_adapter/",
65
63
  ... "sentiment.paw",
66
64
  ... spec="Classify text sentiment as positive, negative, or neutral",