programasweights 0.4.2__tar.gz → 0.4.3__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.
- programasweights-0.4.3/.github/workflows/test.yml +41 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/CHANGELOG.md +12 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/PKG-INFO +1 -1
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/api-reference/python-sdk.md +2 -2
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/api-reference/rest-api.md +6 -1
- programasweights-0.4.3/docs/case-studies/alien-taboo.md +117 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/mkdocs.yml +1 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/__init__.py +1 -1
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/cli.py +16 -24
- {programasweights-0.4.2 → programasweights-0.4.3}/pyproject.toml +1 -1
- programasweights-0.4.3/tests/test_cli_auth.py +186 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/tests/test_sdk.py +60 -102
- {programasweights-0.4.2 → programasweights-0.4.3}/.gitignore +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/.readthedocs.yaml +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/AGENTS.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/LICENSE +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/PYPI_README.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/README.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/adr/001-llama-cpp-over-pytorch.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/adr/002-q4_0-adapter-format.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/adr/003-single-spec-field.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/adr/004-compiler-naming.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/adr/005-vllm-hidden-states.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/adr/006-email-api-key-auth.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/advanced/adrs.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/advanced/architecture.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/api-reference/cli.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/architecture.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/case-studies/log-monitoring.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/case-studies/semantic-search.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/case-studies/site-navigation.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/case-studies/tool-calling.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/getting-started/first-program.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/getting-started/installation.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/getting-started/naming-programs.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/guide/browser-inference.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/guide/how-it-works.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/guide/local-inference.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/guide/writing-good-specs.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/hub/browsing-programs.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/hub/feedback-cases.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/hub/publishing-programs.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/index.md +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/docs/requirements.txt +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/examples/flask_app.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/examples/jupyter_notebook.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/examples/langchain_integration.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/examples/paw_monitor.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/examples/replace_openai.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/_output.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/artifacts.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/cache.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/client.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/compiler/__init__.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/compiler/dummy.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/config.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/convert_peft_to_paw.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/paw_format.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/runtime/__init__.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/runtime/interpreter.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/runtime/interpreter_onnx.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/programasweights/runtime_llamacpp.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/tests/test_offline_cache.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/tests/test_runtime_registry_sdk.py +0 -0
- {programasweights-0.4.2 → programasweights-0.4.3}/tests/test_sdk.sh +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
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"]
|
|
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. The hermetic tests below never import the
|
|
26
|
+
# llama.cpp runtime (it is imported lazily only when running inference).
|
|
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_cli_auth.py \
|
|
40
|
+
tests/test_runtime_registry_sdk.py \
|
|
41
|
+
tests/test_sdk.py::TestInstallAndImport
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.3 (2026-07-06)
|
|
4
|
+
|
|
5
|
+
- Fix `paw info` / `paw rename` crashing with `AttributeError` when run without
|
|
6
|
+
`--api-url`/`--api-key` (they read the removed `paw.api_url`/`paw.api_key`
|
|
7
|
+
module attributes; now resolved via the function-based config API).
|
|
8
|
+
- `--api-url` / `--api-key` now take effect on `compile`, `run`, and `login`
|
|
9
|
+
(previously accepted but silently ignored on those commands).
|
|
10
|
+
- Docs: reference `paw.get_api_url()` / `paw.get_api_key()` instead of the
|
|
11
|
+
removed module attributes.
|
|
12
|
+
- Add hermetic CLI auth test suite (`tests/test_cli_auth.py`) and a GitHub
|
|
13
|
+
Actions CI workflow running it on Python 3.9-3.12.
|
|
14
|
+
|
|
3
15
|
## 0.2.4 (2026-04-01)
|
|
4
16
|
|
|
5
17
|
- Download reliability: asset endpoints return 202 Retry-After when program is still generating, eliminating 404 race conditions
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programasweights
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
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
|
|
@@ -128,8 +128,8 @@ export PAW_API_KEY=paw_sk_...
|
|
|
128
128
|
|
|
129
129
|
| Name | Description |
|
|
130
130
|
|------|-------------|
|
|
131
|
-
| `paw.
|
|
132
|
-
| `paw.
|
|
131
|
+
| `paw.get_api_url()` | Base URL for API requests. Default: `https://programasweights.com`. Override with `PAW_API_URL` env var. |
|
|
132
|
+
| `paw.get_api_key()` | API key for authenticated calls. Set via `paw.login()` or `PAW_API_KEY` env var. |
|
|
133
133
|
| `paw.__version__` | Installed package version string. |
|
|
134
134
|
|
|
135
135
|
## Related
|
|
@@ -158,7 +158,12 @@ Successful responses may include:
|
|
|
158
158
|
| `X-RateLimit-Limit` | Maximum requests per window. |
|
|
159
159
|
| `X-RateLimit-Remaining` | Remaining requests in the current window. |
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
Hosted compile quotas:
|
|
162
|
+
|
|
163
|
+
- Anonymous: **20 compiles/hour**, **1 concurrent compile**
|
|
164
|
+
- Authenticated: **60 compiles/hour**, **2 concurrent compiles**
|
|
165
|
+
|
|
166
|
+
Hosted server-side endpoints may also enforce additional operational concurrency or safety limits. Clients should back off when receiving `429 Too Many Requests` and respect `Retry-After` when present.
|
|
162
167
|
|
|
163
168
|
## Related
|
|
164
169
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Alien Taboo: A Word-Guessing Game on a 0.6B Interpreter
|
|
2
|
+
|
|
3
|
+
Most of our case studies are about *infrastructure* — log triage, search reranking, agent routing. This one is a game. We wanted to show that a tiny PAW program is enough to power a real, playable, in-browser product, not just internal tooling.
|
|
4
|
+
|
|
5
|
+
**Try it live:** [programasweights.com/alien](https://programasweights.com/alien) (English) or [programasweights.com/alien-cn](https://programasweights.com/alien-cn) (中文). You describe a secret word, the alien guesses. The alien is one compiled PAW program running on a 0.6B-parameter interpreter — no GPT-4, no API roulette, just one .paw file and a Qwen3 0.6B base model.
|
|
6
|
+
|
|
7
|
+
## Why this is interesting for PAW
|
|
8
|
+
|
|
9
|
+
Word-guessing is a textbook *fuzzy function*: easy to describe in one sentence, impossible to write as rules. "Given a player's hint, output the secret word." A keyword matcher would never bridge "fluffy thing that purrs" → `cat`. A retrieval system would force you to enumerate every possible hint. A 32B prompted LLM would work but you can't ship one to a browser-tab user for free.
|
|
10
|
+
|
|
11
|
+
A 0.6B interpreter with a focused PAW spec is exactly the right size: small enough to run on a single inference server with no per-request cost, expressive enough to recognise the dozens of ways a child might describe "watermelon."
|
|
12
|
+
|
|
13
|
+
## How we built it
|
|
14
|
+
|
|
15
|
+
### Attempt 1: One huge spec listing every word
|
|
16
|
+
|
|
17
|
+
The first instinct was to bake the whole word list into the spec — "the secret word is one of: cat, dog, banana, …" — so the model only ever picks from a known vocabulary.
|
|
18
|
+
|
|
19
|
+
**Result:** The spec ballooned to thousands of tokens, the compiler timed out, and accuracy *dropped*: the model started biasing toward whichever words appeared earliest in the list regardless of the description.
|
|
20
|
+
|
|
21
|
+
**Lesson:** Don't dump your domain into the spec. PAW programs work best when the spec teaches a *behaviour* with a few examples, not when it tries to be a database.
|
|
22
|
+
|
|
23
|
+
### Attempt 2: Free-form generation with a few examples
|
|
24
|
+
|
|
25
|
+
We replaced the word list with a short instruction and ~15 input/output exemplars covering the diversity of hints (animals, foods, household objects, abstract concepts, multi-word objects).
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
You are playing a word-guessing game. The user describes a common English
|
|
29
|
+
word in their own words without saying the word itself. Your job is to
|
|
30
|
+
guess the word from the description.
|
|
31
|
+
|
|
32
|
+
Return ONLY the single word being described. Lowercase. No punctuation,
|
|
33
|
+
no explanation, no extra words.
|
|
34
|
+
|
|
35
|
+
Input: furry animal that meows and purrs
|
|
36
|
+
Output: cat
|
|
37
|
+
|
|
38
|
+
Input: yellow curved fruit, monkeys like it
|
|
39
|
+
Output: banana
|
|
40
|
+
|
|
41
|
+
Input: thing you use to unlock a door, metal, small, has teeth
|
|
42
|
+
Output: key
|
|
43
|
+
|
|
44
|
+
... (≈12 more)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Result:** Compiled cleanly, ran in ~80 ms per guess, and produced sensible answers for almost every test description. This is the version shipping today.
|
|
48
|
+
|
|
49
|
+
**Lesson:** This is the opposite of what we found in our [site navigation](site-navigation.md) and [semantic search](semantic-search.md) studies — there, generation hallucinated and we reframed as classification. Here, generation works because (a) the answer space is implicit (any English noun) so we couldn't classify even if we wanted to, and (b) the answer is short and over-determined by the input. **Generation works when the output is short and the input is rich.**
|
|
50
|
+
|
|
51
|
+
### Attempt 3: The actual hard part — curating the word bank
|
|
52
|
+
|
|
53
|
+
With the alien working on the first realistic hint we threw at it, we expected to ship in an afternoon. Instead we spent days on word selection. The reason:
|
|
54
|
+
|
|
55
|
+
- **Dead words.** "Sundial" is a word every adult knows, but no one can describe it without saying "sun" or "shadow" or "time," all of which produce the wrong guess. A word that never gets solved isn't unfun for the alien — it's unfun for the *player*. Every dead word in the bank is a session-killing dead end.
|
|
56
|
+
- **Fairness.** "Bullfrog" passes the alien's accuracy test but stumps anyone under 12. Mass-appeal games need words a 6-year-old or non-native speaker can describe.
|
|
57
|
+
- **Variety.** A 50-word bank gets boring after two sessions. We wanted ≥300 viable words.
|
|
58
|
+
|
|
59
|
+
The vetting process became its own pipeline:
|
|
60
|
+
|
|
61
|
+
1. **Generate candidates** with GPT-5.4 (~4000 raw words across 40 themes).
|
|
62
|
+
2. **Simulate playthroughs** — for each candidate, prompt GPT-5.4-mini to play the role of a human describing the word; route those descriptions through the actual deployed alien program; keep words solved within ≤8 rounds across ≥4 of 5 random-seed trials.
|
|
63
|
+
3. **Filter for commonness** with `wordfreq` (Zipf ≥ 5.0) so kids and ESL players have a fair shot.
|
|
64
|
+
4. **Manual pass** by a human (us) on the survivors to remove anything ambiguous, edgy, or culturally narrow.
|
|
65
|
+
|
|
66
|
+
The final bank is 361 ultra-common English words. The full vetting script lives at [`server/scripts/vet_alien_words.py`](https://github.com/programasweights/website/blob/main/server/scripts/vet_alien_words.py).
|
|
67
|
+
|
|
68
|
+
**Lesson:** **The PAW program is usually the easy part of the product.** Treat it as one ingredient. The data, the UI, and the fairness work around the program are typically what take the time.
|
|
69
|
+
|
|
70
|
+
### The multilingual finale: a Chinese version with one new file
|
|
71
|
+
|
|
72
|
+
After the English game shipped, we wanted a Chinese version. The intuition was that this would mean fine-tuning a Chinese-specific compiler, retraining the 0.6B interpreter on a Chinese corpus, or at minimum prompt-engineering around tokeniser quirks.
|
|
73
|
+
|
|
74
|
+
What we actually did:
|
|
75
|
+
|
|
76
|
+
1. Wrote a new spec — `spec_cn.txt` — in Mandarin, with 20 Chinese hint→word examples.
|
|
77
|
+
2. Compiled it with the same `paw-4b-qwen3-0.6b` compiler.
|
|
78
|
+
3. Pointed a sister `/api/v1/alien-cn/guess` endpoint at the new program ID.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import programasweights as paw
|
|
82
|
+
|
|
83
|
+
with open("spec_cn.txt", encoding="utf-8") as f:
|
|
84
|
+
spec = f.read()
|
|
85
|
+
|
|
86
|
+
program = paw.compile(spec, compiler="paw-4b-qwen3-0.6b")
|
|
87
|
+
# That's it. program.id is now ready to serve Chinese players.
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The first compile produced **67.5% per-description accuracy** and **80% at-least-one-of-two accuracy** on a held-out 20-word Chinese test set, comparable to the English program's accuracy on its English test set. We changed exactly zero lines in the SDK or interpreter.
|
|
91
|
+
|
|
92
|
+
**Lesson:** **Spec language is just data.** PAW's compiler doesn't carry English-specific assumptions; the only language-aware things in the system are (a) your spec, and (b) any output post-processing your client does (we had to extend the server's "first-word extractor" from `[a-z]+` to also accept `[\u4e00-\u9fff]+`). For a multilingual product, this is several orders of magnitude less work than maintaining a separate stack per language.
|
|
93
|
+
|
|
94
|
+
## The solution at a glance
|
|
95
|
+
|
|
96
|
+
The entire alien — both languages — is one PAW function per language plus thin glue:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import programasweights as paw
|
|
100
|
+
|
|
101
|
+
alien_en = paw.function(EN_PROGRAM_ID) # compiled from spec.txt
|
|
102
|
+
alien_cn = paw.function(CN_PROGRAM_ID) # compiled from spec_cn.txt
|
|
103
|
+
|
|
104
|
+
def guess(description: str, lang: str) -> str:
|
|
105
|
+
fn = alien_cn if lang == "zh" else alien_en
|
|
106
|
+
return fn(description).strip().lower()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Everything else — the timer, the lives, the share card, the leaderboard — is plain product code. No model orchestration, no prompt-management framework, no second LLM behind it.
|
|
110
|
+
|
|
111
|
+
## Takeaways
|
|
112
|
+
|
|
113
|
+
- **Generation is the right tool when the output is short and the input is rich.** Classification wins for routing; generation wins for "name this thing." Pick the right one for your task.
|
|
114
|
+
- **The PAW program is usually the easy part.** Most product time goes into data quality, UX, and edge cases — exactly as it would with any function in your codebase.
|
|
115
|
+
- **Use small models to vet content for small models.** Running GPT-5.4-mini in a multi-trial simulation against your deployed PAW program is faster and cheaper than human playtesting, and it scales to thousands of candidates overnight.
|
|
116
|
+
- **Spec language is just data.** A new spec in a new language is a new product, with no SDK or interpreter changes required.
|
|
117
|
+
- **A 0.6B interpreter is enough** for tasks where the input over-determines the output. You don't always need 70B; you need the right factoring.
|
|
@@ -52,6 +52,7 @@ nav:
|
|
|
52
52
|
- Publishing Programs: hub/publishing-programs.md
|
|
53
53
|
- Feedback & Cases: hub/feedback-cases.md
|
|
54
54
|
- Case Studies:
|
|
55
|
+
- Alien Taboo (Word Game): case-studies/alien-taboo.md
|
|
55
56
|
- Log Monitoring: case-studies/log-monitoring.md
|
|
56
57
|
- Site Navigation: case-studies/site-navigation.md
|
|
57
58
|
- Semantic Search: case-studies/semantic-search.md
|
|
@@ -14,15 +14,23 @@ All commands support --json for structured output (agent-friendly).
|
|
|
14
14
|
|
|
15
15
|
import argparse
|
|
16
16
|
import json
|
|
17
|
+
import os
|
|
17
18
|
import sys
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
def _apply_auth_overrides(args):
|
|
22
|
+
"""Let the global --api-url/--api-key flags take effect on commands that go
|
|
23
|
+
through the high-level ``paw.compile()``/``paw.function()``/``paw.login()``
|
|
24
|
+
paths, which resolve credentials from the environment/config only."""
|
|
25
|
+
if getattr(args, "api_url", None):
|
|
26
|
+
os.environ["PAW_API_URL"] = args.api_url
|
|
27
|
+
if getattr(args, "api_key", None):
|
|
28
|
+
os.environ["PAW_API_KEY"] = args.api_key
|
|
29
|
+
|
|
30
|
+
|
|
20
31
|
def cmd_compile(args):
|
|
21
32
|
import programasweights as paw
|
|
22
|
-
|
|
23
|
-
paw.api_url = args.api_url
|
|
24
|
-
if args.api_key:
|
|
25
|
-
paw.api_key = args.api_key
|
|
33
|
+
_apply_auth_overrides(args)
|
|
26
34
|
|
|
27
35
|
if not args.json:
|
|
28
36
|
print(f"Compiling: {args.spec[:80]}...")
|
|
@@ -63,10 +71,7 @@ def cmd_compile(args):
|
|
|
63
71
|
|
|
64
72
|
def cmd_run(args):
|
|
65
73
|
import programasweights as paw
|
|
66
|
-
|
|
67
|
-
paw.api_url = args.api_url
|
|
68
|
-
if args.api_key:
|
|
69
|
-
paw.api_key = args.api_key
|
|
74
|
+
_apply_auth_overrides(args)
|
|
70
75
|
|
|
71
76
|
fn = paw.function(
|
|
72
77
|
args.program, verbose=args.verbose,
|
|
@@ -82,22 +87,15 @@ def cmd_run(args):
|
|
|
82
87
|
|
|
83
88
|
def cmd_login(args):
|
|
84
89
|
import programasweights as paw
|
|
85
|
-
|
|
86
|
-
paw.api_url = args.api_url
|
|
90
|
+
_apply_auth_overrides(args)
|
|
87
91
|
paw.login(args.key)
|
|
88
92
|
return 0
|
|
89
93
|
|
|
90
94
|
|
|
91
95
|
def cmd_rename(args):
|
|
92
|
-
import programasweights as paw
|
|
93
|
-
if args.api_url:
|
|
94
|
-
paw.api_url = args.api_url
|
|
95
|
-
if args.api_key:
|
|
96
|
-
paw.api_key = args.api_key
|
|
97
|
-
|
|
98
96
|
import httpx
|
|
99
97
|
from programasweights.client import PAWClient
|
|
100
|
-
client = PAWClient(api_url=
|
|
98
|
+
client = PAWClient(api_url=args.api_url, api_key=args.api_key)
|
|
101
99
|
|
|
102
100
|
resp = httpx.patch(
|
|
103
101
|
f"{client._api_url}/api/v1/programs/{args.program}",
|
|
@@ -120,14 +118,8 @@ def cmd_rename(args):
|
|
|
120
118
|
|
|
121
119
|
|
|
122
120
|
def cmd_info(args):
|
|
123
|
-
import programasweights as paw
|
|
124
|
-
if args.api_url:
|
|
125
|
-
paw.api_url = args.api_url
|
|
126
|
-
if args.api_key:
|
|
127
|
-
paw.api_key = args.api_key
|
|
128
|
-
|
|
129
121
|
from programasweights.client import PAWClient
|
|
130
|
-
client = PAWClient(api_url=
|
|
122
|
+
client = PAWClient(api_url=args.api_url, api_key=args.api_key)
|
|
131
123
|
|
|
132
124
|
try:
|
|
133
125
|
meta = client.get_program_meta(args.program)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "programasweights"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.3"
|
|
8
8
|
description = "Compile natural language specifications into neural programs that run locally via llama.cpp."
|
|
9
9
|
readme = "PYPI_README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI authentication plumbing tests.
|
|
3
|
+
|
|
4
|
+
These are hermetic: no network, no model load. They cover two classes of bug:
|
|
5
|
+
|
|
6
|
+
1. Regression guard for the AttributeError crash fixed in PR #1 -- `paw info`
|
|
7
|
+
and `paw rename` used to read the removed module attributes
|
|
8
|
+
`paw.api_url`/`paw.api_key` and crashed when no flags were given.
|
|
9
|
+
|
|
10
|
+
2. `--api-url`/`--api-key` must actually take effect on `compile`, `run`, and
|
|
11
|
+
`login`. Before the 0.4.3 fix these were global flags that the high-level
|
|
12
|
+
`paw.compile()`/`paw.function()`/`paw.login()` paths silently ignored
|
|
13
|
+
(they only read env/config). The flag-plumbing tests below fail on that
|
|
14
|
+
older behavior and pass once the flags are wired through the environment.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from types import SimpleNamespace
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
import programasweights as paw
|
|
22
|
+
from programasweights import cli, config
|
|
23
|
+
import programasweights.client as paw_client
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FakeProgram:
|
|
27
|
+
id = "prog123"
|
|
28
|
+
slug = None
|
|
29
|
+
status = "ready"
|
|
30
|
+
error = None
|
|
31
|
+
timings = None
|
|
32
|
+
version = 1
|
|
33
|
+
version_action = "created"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _install_fake_client(monkeypatch, sink):
|
|
37
|
+
"""Replace PAWClient with a fake that records the api_url/api_key it was
|
|
38
|
+
constructed with, and returns canned data instead of hitting the network."""
|
|
39
|
+
|
|
40
|
+
class FakeClient:
|
|
41
|
+
def __init__(self, api_url=None, api_key=None):
|
|
42
|
+
sink["api_url"] = api_url
|
|
43
|
+
sink["api_key"] = api_key
|
|
44
|
+
self._api_url = (api_url or "https://programasweights.com").rstrip("/")
|
|
45
|
+
self._api_key = api_key
|
|
46
|
+
|
|
47
|
+
def _headers(self):
|
|
48
|
+
return {"Content-Type": "application/json"}
|
|
49
|
+
|
|
50
|
+
def get_program_meta(self, program):
|
|
51
|
+
return {"id": program, "spec": "canned"}
|
|
52
|
+
|
|
53
|
+
def compile(self, *a, **k):
|
|
54
|
+
return FakeProgram()
|
|
55
|
+
|
|
56
|
+
monkeypatch.setattr(paw_client, "PAWClient", FakeClient)
|
|
57
|
+
return sink
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture(autouse=True)
|
|
61
|
+
def _hermetic_env(monkeypatch):
|
|
62
|
+
# Ignore any real ~/.config/programasweights/config.json and env so the
|
|
63
|
+
# only source of api_url/api_key is what the command threads through.
|
|
64
|
+
monkeypatch.setattr(config, "_load_config", lambda: {})
|
|
65
|
+
monkeypatch.delenv("PAW_API_KEY", raising=False)
|
|
66
|
+
monkeypatch.delenv("PAW_API_URL", raising=False)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── Regression guards: info/rename must not crash without flags ──
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_cmd_info_no_flags_no_attributeerror(monkeypatch):
|
|
73
|
+
sink = _install_fake_client(monkeypatch, {})
|
|
74
|
+
args = SimpleNamespace(program="prog123", api_url=None, api_key=None, json=True)
|
|
75
|
+
rc = cli.cmd_info(args) # crashed with AttributeError before PR #1
|
|
76
|
+
assert rc == 0
|
|
77
|
+
assert sink["api_url"] is None and sink["api_key"] is None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_cmd_rename_no_flags_no_attributeerror(monkeypatch):
|
|
81
|
+
sink = _install_fake_client(monkeypatch, {})
|
|
82
|
+
|
|
83
|
+
import httpx
|
|
84
|
+
|
|
85
|
+
class FakeResp:
|
|
86
|
+
def raise_for_status(self):
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
def json(self):
|
|
90
|
+
return {"slug": "new-slug"}
|
|
91
|
+
|
|
92
|
+
monkeypatch.setattr(httpx, "patch", lambda *a, **k: FakeResp())
|
|
93
|
+
args = SimpleNamespace(
|
|
94
|
+
program="prog123", new_slug="new-slug", api_url=None, api_key=None, json=True
|
|
95
|
+
)
|
|
96
|
+
rc = cli.cmd_rename(args) # crashed with AttributeError before PR #1
|
|
97
|
+
assert rc == 0
|
|
98
|
+
assert sink["api_url"] is None and sink["api_key"] is None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_cmd_info_honors_flags(monkeypatch):
|
|
102
|
+
sink = _install_fake_client(monkeypatch, {})
|
|
103
|
+
args = SimpleNamespace(
|
|
104
|
+
program="prog123",
|
|
105
|
+
api_url="http://test.local",
|
|
106
|
+
api_key="paw_sk_test",
|
|
107
|
+
json=True,
|
|
108
|
+
)
|
|
109
|
+
cli.cmd_info(args)
|
|
110
|
+
assert sink["api_url"] == "http://test.local"
|
|
111
|
+
assert sink["api_key"] == "paw_sk_test"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── Flag plumbing: compile/run/login must honor --api-url/--api-key ──
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_apply_auth_overrides_sets_env(monkeypatch):
|
|
118
|
+
cli._apply_auth_overrides(
|
|
119
|
+
SimpleNamespace(api_url="http://test.local", api_key="paw_sk_test")
|
|
120
|
+
)
|
|
121
|
+
assert config.get_api_url() == "http://test.local"
|
|
122
|
+
assert config.get_api_key() == "paw_sk_test"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_apply_auth_overrides_ignores_absent(monkeypatch):
|
|
126
|
+
cli._apply_auth_overrides(SimpleNamespace(api_url=None, api_key=None))
|
|
127
|
+
# Falls back to default url, no key.
|
|
128
|
+
assert config.get_api_url() == "https://programasweights.com"
|
|
129
|
+
assert config.get_api_key() is None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_compile_flag_plumbing(monkeypatch):
|
|
133
|
+
sink = _install_fake_client(monkeypatch, {})
|
|
134
|
+
args = SimpleNamespace(
|
|
135
|
+
spec="Classify sentiment as positive or negative.",
|
|
136
|
+
compiler=None,
|
|
137
|
+
slug=None,
|
|
138
|
+
private=False,
|
|
139
|
+
api_url="http://test.local",
|
|
140
|
+
api_key="paw_sk_test",
|
|
141
|
+
json=True,
|
|
142
|
+
)
|
|
143
|
+
cli.cmd_compile(args)
|
|
144
|
+
# The flag must reach the client through get_api_url()/get_api_key().
|
|
145
|
+
assert sink["api_url"] == "http://test.local"
|
|
146
|
+
assert sink["api_key"] == "paw_sk_test"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_run_flag_plumbing(monkeypatch):
|
|
150
|
+
seen = {}
|
|
151
|
+
|
|
152
|
+
def fake_function(program_id, **kwargs):
|
|
153
|
+
seen["api_url"] = config.get_api_url()
|
|
154
|
+
seen["api_key"] = config.get_api_key()
|
|
155
|
+
return lambda *a, **k: "ok"
|
|
156
|
+
|
|
157
|
+
monkeypatch.setattr(paw, "function", fake_function)
|
|
158
|
+
args = SimpleNamespace(
|
|
159
|
+
program="prog123",
|
|
160
|
+
input="hello",
|
|
161
|
+
max_tokens=512,
|
|
162
|
+
temperature=0.0,
|
|
163
|
+
verbose=False,
|
|
164
|
+
api_url="http://test.local",
|
|
165
|
+
api_key="paw_sk_test",
|
|
166
|
+
json=True,
|
|
167
|
+
)
|
|
168
|
+
cli.cmd_run(args)
|
|
169
|
+
assert seen["api_url"] == "http://test.local"
|
|
170
|
+
assert seen["api_key"] == "paw_sk_test"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_login_flag_plumbing(monkeypatch):
|
|
174
|
+
seen = {}
|
|
175
|
+
|
|
176
|
+
def fake_login(key=None):
|
|
177
|
+
seen["api_url"] = config.get_api_url()
|
|
178
|
+
seen["key"] = key
|
|
179
|
+
|
|
180
|
+
monkeypatch.setattr(paw, "login", fake_login)
|
|
181
|
+
args = SimpleNamespace(
|
|
182
|
+
key="paw_sk_test", api_url="http://test.local", api_key=None
|
|
183
|
+
)
|
|
184
|
+
cli.cmd_login(args)
|
|
185
|
+
assert seen["api_url"] == "http://test.local"
|
|
186
|
+
assert seen["key"] == "paw_sk_test"
|
|
@@ -291,31 +291,29 @@ class TestAuth:
|
|
|
291
291
|
def test_compile_with_slug_authenticated(self):
|
|
292
292
|
import time
|
|
293
293
|
slug = f"test-sdk-{int(time.time()) % 100000}"
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
assert program.id
|
|
302
|
-
assert program.slug
|
|
303
|
-
assert slug in program.slug
|
|
304
|
-
finally:
|
|
305
|
-
paw.api_key = old_key
|
|
294
|
+
program = paw.compile(
|
|
295
|
+
"Classify text as positive or negative sentiment.",
|
|
296
|
+
slug=slug,
|
|
297
|
+
)
|
|
298
|
+
assert program.id
|
|
299
|
+
assert program.slug
|
|
300
|
+
assert slug in program.slug
|
|
306
301
|
|
|
307
302
|
@needs_auth
|
|
308
303
|
def test_compile_with_slug_bad_key(self):
|
|
309
|
-
old_key =
|
|
304
|
+
old_key = os.environ.get("PAW_API_KEY")
|
|
305
|
+
os.environ["PAW_API_KEY"] = "paw_sk_invalid_key_12345"
|
|
310
306
|
try:
|
|
311
|
-
paw.api_key = "paw_sk_invalid_key_12345"
|
|
312
307
|
with pytest.raises(Exception):
|
|
313
308
|
paw.compile(
|
|
314
309
|
"Classify text as positive or negative sentiment.",
|
|
315
310
|
slug="should-fail-auth",
|
|
316
311
|
)
|
|
317
312
|
finally:
|
|
318
|
-
|
|
313
|
+
if old_key is None:
|
|
314
|
+
del os.environ["PAW_API_KEY"]
|
|
315
|
+
else:
|
|
316
|
+
os.environ["PAW_API_KEY"] = old_key
|
|
319
317
|
|
|
320
318
|
@needs_auth
|
|
321
319
|
def test_cli_compile_with_api_key_flag(self):
|
|
@@ -336,34 +334,19 @@ class TestAuth:
|
|
|
336
334
|
class TestSlugValidation:
|
|
337
335
|
@needs_auth
|
|
338
336
|
def test_slug_too_short(self):
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
with pytest.raises(Exception) as exc_info:
|
|
343
|
-
paw.compile("Classify text.", slug="x")
|
|
344
|
-
assert "422" in str(exc_info.value) or "invalid" in str(exc_info.value).lower()
|
|
345
|
-
finally:
|
|
346
|
-
paw.api_key = old_key
|
|
337
|
+
with pytest.raises(Exception) as exc_info:
|
|
338
|
+
paw.compile("Classify text.", slug="x")
|
|
339
|
+
assert "422" in str(exc_info.value) or "invalid" in str(exc_info.value).lower()
|
|
347
340
|
|
|
348
341
|
@needs_auth
|
|
349
342
|
def test_slug_invalid_characters(self):
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
paw.api_key = API_KEY
|
|
353
|
-
with pytest.raises(Exception):
|
|
354
|
-
paw.compile("Classify text.", slug="BAD SLUG!")
|
|
355
|
-
finally:
|
|
356
|
-
paw.api_key = old_key
|
|
343
|
+
with pytest.raises(Exception):
|
|
344
|
+
paw.compile("Classify text.", slug="BAD SLUG!")
|
|
357
345
|
|
|
358
346
|
@needs_auth
|
|
359
347
|
def test_slug_with_uppercase_rejected(self):
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
paw.api_key = API_KEY
|
|
363
|
-
with pytest.raises(Exception):
|
|
364
|
-
paw.compile("Classify text.", slug="MyProgram")
|
|
365
|
-
finally:
|
|
366
|
-
paw.api_key = old_key
|
|
348
|
+
with pytest.raises(Exception):
|
|
349
|
+
paw.compile("Classify text.", slug="MyProgram")
|
|
367
350
|
|
|
368
351
|
|
|
369
352
|
# ── Phase 10: Privacy ──
|
|
@@ -373,36 +356,26 @@ class TestPrivacy:
|
|
|
373
356
|
@needs_auth
|
|
374
357
|
def test_compile_private_program(self):
|
|
375
358
|
import httpx
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
timeout=10.0,
|
|
385
|
-
)
|
|
386
|
-
assert resp.status_code == 404, "Private program should return 404 to unauthenticated users"
|
|
387
|
-
finally:
|
|
388
|
-
paw.api_key = old_key
|
|
359
|
+
program = paw.compile("Private test program for counting vowels.", public=False)
|
|
360
|
+
assert program.id
|
|
361
|
+
|
|
362
|
+
resp = httpx.get(
|
|
363
|
+
f"{paw.get_api_url()}/api/v1/programs/{program.id}",
|
|
364
|
+
timeout=10.0,
|
|
365
|
+
)
|
|
366
|
+
assert resp.status_code == 404, "Private program should return 404 to unauthenticated users"
|
|
389
367
|
|
|
390
368
|
@needs_auth
|
|
391
369
|
def test_compile_public_default(self):
|
|
392
370
|
import httpx
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
timeout=10.0,
|
|
402
|
-
)
|
|
403
|
-
assert resp.status_code == 200, "Public program should be accessible"
|
|
404
|
-
finally:
|
|
405
|
-
paw.api_key = old_key
|
|
371
|
+
program = paw.compile("Public test program for counting words.")
|
|
372
|
+
assert program.id
|
|
373
|
+
|
|
374
|
+
resp = httpx.get(
|
|
375
|
+
f"{paw.get_api_url()}/api/v1/programs/{program.id}",
|
|
376
|
+
timeout=10.0,
|
|
377
|
+
)
|
|
378
|
+
assert resp.status_code == 200, "Public program should be accessible"
|
|
406
379
|
|
|
407
380
|
|
|
408
381
|
# ── Phase 11: CLI rename ──
|
|
@@ -412,24 +385,19 @@ class TestCLIRename:
|
|
|
412
385
|
@needs_auth
|
|
413
386
|
def test_cli_rename(self):
|
|
414
387
|
import time
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
data = json.loads(result.stdout)
|
|
429
|
-
assert data.get("slug")
|
|
430
|
-
assert slug in data["slug"]
|
|
431
|
-
finally:
|
|
432
|
-
paw.api_key = old_key
|
|
388
|
+
program = paw.compile("Test rename program for validation.")
|
|
389
|
+
slug = f"rename-test-{int(time.time()) % 100000}"
|
|
390
|
+
|
|
391
|
+
result = subprocess.run(
|
|
392
|
+
[sys.executable, "-m", "programasweights.cli",
|
|
393
|
+
"--api-key", API_KEY,
|
|
394
|
+
"rename", program.id, slug, "--json"],
|
|
395
|
+
capture_output=True, text=True,
|
|
396
|
+
)
|
|
397
|
+
assert result.returncode == 0
|
|
398
|
+
data = json.loads(result.stdout)
|
|
399
|
+
assert data.get("slug")
|
|
400
|
+
assert slug in data["slug"]
|
|
433
401
|
|
|
434
402
|
@needs_auth
|
|
435
403
|
def test_cli_compile_private(self):
|
|
@@ -481,30 +449,20 @@ class TestFunctionAcceptsProgram:
|
|
|
481
449
|
class TestListPrograms:
|
|
482
450
|
@needs_auth
|
|
483
451
|
def test_list_programs_returns_dict(self):
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
assert "programs" in result
|
|
490
|
-
assert "total" in result
|
|
491
|
-
assert isinstance(result["programs"], list)
|
|
492
|
-
finally:
|
|
493
|
-
paw.api_key = old_key
|
|
452
|
+
result = paw.list_programs()
|
|
453
|
+
assert isinstance(result, dict)
|
|
454
|
+
assert "programs" in result
|
|
455
|
+
assert "total" in result
|
|
456
|
+
assert isinstance(result["programs"], list)
|
|
494
457
|
|
|
495
458
|
@needs_auth
|
|
496
459
|
def test_list_programs_has_entries(self):
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
assert
|
|
502
|
-
|
|
503
|
-
p = result["programs"][0]
|
|
504
|
-
assert "id" in p
|
|
505
|
-
assert "spec" in p
|
|
506
|
-
finally:
|
|
507
|
-
paw.api_key = old_key
|
|
460
|
+
result = paw.list_programs(per_page=5)
|
|
461
|
+
assert len(result["programs"]) <= 5
|
|
462
|
+
if result["programs"]:
|
|
463
|
+
p = result["programs"][0]
|
|
464
|
+
assert "id" in p
|
|
465
|
+
assert "spec" in p
|
|
508
466
|
|
|
509
467
|
|
|
510
468
|
class TestStderrSuppression:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programasweights-0.4.2 → programasweights-0.4.3}/programasweights/runtime/interpreter_onnx.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|