stratyfy-cli 0.1.0__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.
- stratyfy_cli-0.1.0/PKG-INFO +119 -0
- stratyfy_cli-0.1.0/README.md +106 -0
- stratyfy_cli-0.1.0/hatch_build.py +20 -0
- stratyfy_cli-0.1.0/pre_api_client/__init__.py +8 -0
- stratyfy_cli-0.1.0/pre_api_client/api/__init__.py +1 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/__init__.py +1 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/create_dataset.py +184 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/create_model_version.py +197 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/get_dataset.py +166 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/get_device_token.py +178 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/get_model.py +166 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/get_workspace.py +134 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/list_datasets.py +134 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/list_models.py +166 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/list_rules.py +171 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/predict.py +197 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/start_device_login.py +128 -0
- stratyfy_cli-0.1.0/pre_api_client/api/default/train_model.py +176 -0
- stratyfy_cli-0.1.0/pre_api_client/client.py +280 -0
- stratyfy_cli-0.1.0/pre_api_client/errors.py +16 -0
- stratyfy_cli-0.1.0/pre_api_client/models/__init__.py +69 -0
- stratyfy_cli-0.1.0/pre_api_client/models/clause.py +128 -0
- stratyfy_cli-0.1.0/pre_api_client/models/clause_operator.py +18 -0
- stratyfy_cli-0.1.0/pre_api_client/models/column.py +87 -0
- stratyfy_cli-0.1.0/pre_api_client/models/column_domain.py +10 -0
- stratyfy_cli-0.1.0/pre_api_client/models/create_dataset_body.py +85 -0
- stratyfy_cli-0.1.0/pre_api_client/models/create_model_version_body.py +75 -0
- stratyfy_cli-0.1.0/pre_api_client/models/dataset.py +116 -0
- stratyfy_cli-0.1.0/pre_api_client/models/device_code.py +101 -0
- stratyfy_cli-0.1.0/pre_api_client/models/device_token.py +93 -0
- stratyfy_cli-0.1.0/pre_api_client/models/device_token_token_type.py +8 -0
- stratyfy_cli-0.1.0/pre_api_client/models/error.py +81 -0
- stratyfy_cli-0.1.0/pre_api_client/models/error_code.py +25 -0
- stratyfy_cli-0.1.0/pre_api_client/models/get_device_token_body.py +61 -0
- stratyfy_cli-0.1.0/pre_api_client/models/list_datasets_response_200.py +75 -0
- stratyfy_cli-0.1.0/pre_api_client/models/list_models_response_200.py +75 -0
- stratyfy_cli-0.1.0/pre_api_client/models/list_rules_response_200.py +75 -0
- stratyfy_cli-0.1.0/pre_api_client/models/metrics.py +129 -0
- stratyfy_cli-0.1.0/pre_api_client/models/metrics_calibration_item.py +77 -0
- stratyfy_cli-0.1.0/pre_api_client/models/metrics_lift_item.py +77 -0
- stratyfy_cli-0.1.0/pre_api_client/models/model.py +234 -0
- stratyfy_cli-0.1.0/pre_api_client/models/model_status.py +11 -0
- stratyfy_cli-0.1.0/pre_api_client/models/predict_body.py +79 -0
- stratyfy_cli-0.1.0/pre_api_client/models/predict_body_rows_item.py +45 -0
- stratyfy_cli-0.1.0/pre_api_client/models/predict_response_200.py +75 -0
- stratyfy_cli-0.1.0/pre_api_client/models/prediction.py +112 -0
- stratyfy_cli-0.1.0/pre_api_client/models/prediction_fired_rules_item.py +99 -0
- stratyfy_cli-0.1.0/pre_api_client/models/rule.py +138 -0
- stratyfy_cli-0.1.0/pre_api_client/models/rule_change.py +95 -0
- stratyfy_cli-0.1.0/pre_api_client/models/rule_change_action.py +10 -0
- stratyfy_cli-0.1.0/pre_api_client/models/rule_input.py +96 -0
- stratyfy_cli-0.1.0/pre_api_client/models/train_request.py +94 -0
- stratyfy_cli-0.1.0/pre_api_client/models/workspace.py +69 -0
- stratyfy_cli-0.1.0/pre_api_client/types.py +54 -0
- stratyfy_cli-0.1.0/pre_cli/__init__.py +0 -0
- stratyfy_cli-0.1.0/pre_cli/config.py +31 -0
- stratyfy_cli-0.1.0/pre_cli/data.py +92 -0
- stratyfy_cli-0.1.0/pre_cli/main.py +749 -0
- stratyfy_cli-0.1.0/pre_cli/sandbox.py +94 -0
- stratyfy_cli-0.1.0/pre_cli/schema.py +72 -0
- stratyfy_cli-0.1.0/pre_report/__init__.py +4 -0
- stratyfy_cli-0.1.0/pre_report/render.py +124 -0
- stratyfy_cli-0.1.0/pre_report/template.html +49 -0
- stratyfy_cli-0.1.0/pre_report/types.py +30 -0
- stratyfy_cli-0.1.0/pre_words/__init__.py +105 -0
- stratyfy_cli-0.1.0/pyproject.toml +47 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: stratyfy-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The pre command: describe a database, train a PRE model, read its rules
|
|
5
|
+
Requires-Python: <4,>=3.11
|
|
6
|
+
Requires-Dist: attrs>=23
|
|
7
|
+
Requires-Dist: httpx<1,>=0.27
|
|
8
|
+
Requires-Dist: psycopg[binary]<4,>=3.1
|
|
9
|
+
Requires-Dist: python-dateutil>=2.8
|
|
10
|
+
Requires-Dist: sqlalchemy<3,>=2.0
|
|
11
|
+
Requires-Dist: typer>=0.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# pre-cli
|
|
15
|
+
|
|
16
|
+
The `pre` command. A coding agent runs it to describe the user's database, train a PRE model, read its rules and
|
|
17
|
+
score cases.
|
|
18
|
+
It talks to the PRE API through `packages/pre-api-client`. It never imports PRE. Its sentences come from
|
|
19
|
+
`packages/pre-words`, so they match the report.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pipx install stratyfy-cli # or: uv tool install stratyfy-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For development, run `poetry install` in this folder, then `poetry run pre --help`. `pre --version` prints the
|
|
28
|
+
version.
|
|
29
|
+
|
|
30
|
+
The wheel and the sdist bundle `pre_api_client`, `pre_words` and `pre_report` from `packages/`, so the install
|
|
31
|
+
needs nothing else from this repo. `hatch_build.py` copies them in at build time. The CLI uses hatchling to build,
|
|
32
|
+
because poetry-core cannot include files from outside this folder. Poetry still manages the development env.
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
Each command prints a short text result and a next step. Add `--json` to get one JSON object instead. On
|
|
37
|
+
failure the command exits with 1 and prints `code`, `message` and `next_step`.
|
|
38
|
+
|
|
39
|
+
| Command | What it does |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `pre login [--no-wait] [--api-url <url>]` | Signs in as `gh auth login` does. It prints a link and a code. The user signs in with an email code and approves the device. Then it writes a new key for this machine to `.pre/config.json`. With `--no-wait` it prints the link and exits; the next `pre login` waits for that same sign-in. That way an agent can show the user the link first. The default API is `https://api.stratyfy.ai/v1`. |
|
|
42
|
+
| `pre logout` | Removes the key from `.pre/config.json`. The key keeps working until an admin revokes it, so the output names its id. |
|
|
43
|
+
| `pre init --api-key <key> [--api-url <url>]` | Writes a key you already have to `.pre/config.json`. The key can also come from `PRE_API_KEY`. |
|
|
44
|
+
| `pre connect <db>` | Describes a SQLite or Postgres database: tables, row counts, columns, types, null share, sample values and foreign keys. It opens the database read-only. |
|
|
45
|
+
| `pre setup claude [--yes] [--dry-run] [--local]` | For Claude Code. Run it in your own terminal. It adds the API host and a remote Postgres host to Claude Code's sandbox network allowlist. It shows the change and asks first. It refuses to write when an agent runs it. See [Sandboxes](#sandboxes). |
|
|
46
|
+
| `pre status` | Checks that the API answers and accepts the key, and names the workspace. Lists the datasets and models this folder made. |
|
|
47
|
+
| `pre outcome define --name <slug> --sql <query> [--case account --cases accounts --event churn --event-past churned]` | Runs the outcome query and stores it. The query returns one row per case: the case id, then a 0/1 label. It refuses no rows, a label that is not 0 or 1, a repeated case id and a label that is always the same. The words go into every sentence the CLI prints. It guesses each word you leave out from the column names and says so. |
|
|
48
|
+
| `pre outcome show` | Lists the stored outcomes and their queries. |
|
|
49
|
+
| `pre extract --outcome <slug> --features <query>` | Runs the features query (the case id, then the features), joins it to the label and writes `.pre/data/<slug>.csv`. Nothing is sent. |
|
|
50
|
+
| `pre extract --outcome <slug> --upload [--refresh]` | Sends the CSV the dry run wrote, unchanged. With `--refresh`, or with no CSV yet, it runs the queries again first. |
|
|
51
|
+
| `pre train --outcome <slug> [--max-rules N] [--max-conditions N] [--no-wait] [--timeout S]` | Trains on the outcome's uploaded dataset. Every column but `id` is an input. It waits until the model is ready or failed and prints progress to stderr. When ready, it says how well the model predicts on held-out cases: the AUC and how many times the average rate the top 10% hold. |
|
|
52
|
+
| `pre rules --outcome <slug>` or `pre rules --model <id>` | Prints the rules as sentences, strongest first, with points and direction. The baseline shows as the starting rate. `--json` gives the API's rules. |
|
|
53
|
+
| `pre predict --outcome <slug> [--features <query>]` | Scores cases with the outcome's model. The query returns the case id, then the model inputs as of today, for example for the accounts still active. Without `--features` it scores the training cases as of their snapshot date and says so. It writes `.pre/data/<slug>.scores.csv`: the case id, the score, the points and the rules that fired as sentences, highest score first. It also writes `.pre/data/<slug>.scores.json` for `pre report`: the same scores with the rule ids that fired. It refuses a query that lacks an input or repeats a case id. |
|
|
54
|
+
| `pre report --outcome <slug> [--actions <file.json>] [--labels <file.json>] [--top N]` | Writes `.pre/reports/<slug>.html` and `.pre/reports/<slug>.csv` from the scores `pre predict` saved. It sends nothing. The page says how well the model predicts, lists the top rules as sentences and the `N` cases most likely to have the outcome, with their reasons. The CSV holds the same cases. `--actions` maps a rule id to one suggested action. `--labels` maps an input column to a plain label. It refuses a rule id or a column the model lacks. When the scores came from the training cases, the page says so. |
|
|
55
|
+
|
|
56
|
+
`<db>` is a SQLite file path, `sqlite:///path`, or `postgresql://user:password@host:5432/db`. Use `--schema` for
|
|
57
|
+
a Postgres schema other than `public`.
|
|
58
|
+
|
|
59
|
+
A `<query>` is the SQL itself or a path that ends in `.sql`. It must be one SELECT or WITH statement. The CLI
|
|
60
|
+
opens SQLite with `mode=ro` and runs Postgres in a read-only transaction, so a query cannot change data. For
|
|
61
|
+
Postgres, a database user with read access only is safer still.
|
|
62
|
+
|
|
63
|
+
## What leaves the machine
|
|
64
|
+
|
|
65
|
+
The CSV that `pre extract --upload` sends, and the rows that `pre predict` sends. Before it writes the CSV, `pre extract`:
|
|
66
|
+
|
|
67
|
+
1. Drops a column when every word of its name is an identifier word and one of them is a core word. Core words
|
|
68
|
+
are `email`, `phone`, `mobile`, `ip`, `address`, `street`, `ssn`, `name` and `username`. The other identifier
|
|
69
|
+
words are `mail`, `number`, `first`, `last`, `full`, `user`, `contact`, `home` and `work`. So `work_email` and
|
|
70
|
+
`first_name` go, and `plan_name` and `address_verified` stay.
|
|
71
|
+
2. Drops a column when a text value holds an email address or looks like a phone number.
|
|
72
|
+
3. Replaces the case id with an HMAC of it in a column named `id`. The key is in `.pre/secret` (mode 600).
|
|
73
|
+
`.pre/data/<slug>.ids.csv` maps each hash back to the case id. It stays on this machine.
|
|
74
|
+
|
|
75
|
+
The output names each dropped column and the reason.
|
|
76
|
+
|
|
77
|
+
`pre predict` sends each case as the same HMAC of its id and the model's inputs, in batches of 1,000. It sends no
|
|
78
|
+
other column. It maps the hashes back to the case ids in memory.
|
|
79
|
+
|
|
80
|
+
## Sandboxes
|
|
81
|
+
|
|
82
|
+
`pre setup` writes the smallest change it can. It merges into the file, so a second run changes nothing.
|
|
83
|
+
|
|
84
|
+
It writes `.claude/settings.json`, key `sandbox.network.allowedDomains`. For a Postgres on this machine, `--local`
|
|
85
|
+
also sets `sandbox.network.allowLocalBinding`, which opens every local port. It never sets `allowAllUnixSockets`.
|
|
86
|
+
Claude Code is the only agent at launch; others are on the roadmap. The onboard skill comes with the plugin
|
|
87
|
+
(`tools/agent-plugin`), not with `pre setup`.
|
|
88
|
+
|
|
89
|
+
These key names come from secondary sources. They live in `pre_cli/sandbox.py`.
|
|
90
|
+
|
|
91
|
+
## Local config
|
|
92
|
+
|
|
93
|
+
`.pre/config.json` holds the API URL, the key and its id, the database URL, the outcome queries and the dataset
|
|
94
|
+
and model ids that later commands create. While a `pre login --no-wait` waits, it also holds that sign-in. The API
|
|
95
|
+
can also list every dataset and model in the workspace. Each command that writes the config also writes
|
|
96
|
+
`.pre/.gitignore`, so git ignores the folder. Set `PRE_DIR` to use another folder.
|
|
97
|
+
|
|
98
|
+
## Tests
|
|
99
|
+
|
|
100
|
+
`scripts/check pre-cli` runs ruff and pytest. The `pre status` tests start a Prism mock of
|
|
101
|
+
`services/pre-api/openapi.yaml` with `npx`. They skip when `npx` is missing.
|
|
102
|
+
|
|
103
|
+
`tests/test_package.py` builds the sdist, a wheel from that sdist and a wheel from this folder. It fails if one
|
|
104
|
+
holds a file outside `pre_cli` and the three bundled packages, a `services`, `apps` or `ground_truth` path, a
|
|
105
|
+
`seed.py` or `check.py`, or the name of the PRE library. PRE must never reach PyPI.
|
|
106
|
+
|
|
107
|
+
## Release
|
|
108
|
+
|
|
109
|
+
The CLI is on PyPI as `stratyfy-cli`. The command it installs is `pre`. `.github/workflows/release-cli.yml`
|
|
110
|
+
publishes it with trusted publishing, so no PyPI token is stored.
|
|
111
|
+
|
|
112
|
+
1. Set the new `version` in `pyproject.toml`. It is the only place that holds the version.
|
|
113
|
+
2. Merge that change to `master`. CI runs `scripts/check pre-cli`, which builds the package and checks its files.
|
|
114
|
+
3. Tag the merge commit and push the tag: `git tag stratyfy-cli-v<version>`, then
|
|
115
|
+
`git push origin stratyfy-cli-v<version>`.
|
|
116
|
+
4. The workflow checks that the tag matches the version, builds, installs the wheel and runs `pre --version`. Then
|
|
117
|
+
it publishes in the GitHub environment `pypi`.
|
|
118
|
+
|
|
119
|
+
Users install it with Python 3.11 or later: `pipx install stratyfy-cli`.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# pre-cli
|
|
2
|
+
|
|
3
|
+
The `pre` command. A coding agent runs it to describe the user's database, train a PRE model, read its rules and
|
|
4
|
+
score cases.
|
|
5
|
+
It talks to the PRE API through `packages/pre-api-client`. It never imports PRE. Its sentences come from
|
|
6
|
+
`packages/pre-words`, so they match the report.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pipx install stratyfy-cli # or: uv tool install stratyfy-cli
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
For development, run `poetry install` in this folder, then `poetry run pre --help`. `pre --version` prints the
|
|
15
|
+
version.
|
|
16
|
+
|
|
17
|
+
The wheel and the sdist bundle `pre_api_client`, `pre_words` and `pre_report` from `packages/`, so the install
|
|
18
|
+
needs nothing else from this repo. `hatch_build.py` copies them in at build time. The CLI uses hatchling to build,
|
|
19
|
+
because poetry-core cannot include files from outside this folder. Poetry still manages the development env.
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
Each command prints a short text result and a next step. Add `--json` to get one JSON object instead. On
|
|
24
|
+
failure the command exits with 1 and prints `code`, `message` and `next_step`.
|
|
25
|
+
|
|
26
|
+
| Command | What it does |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `pre login [--no-wait] [--api-url <url>]` | Signs in as `gh auth login` does. It prints a link and a code. The user signs in with an email code and approves the device. Then it writes a new key for this machine to `.pre/config.json`. With `--no-wait` it prints the link and exits; the next `pre login` waits for that same sign-in. That way an agent can show the user the link first. The default API is `https://api.stratyfy.ai/v1`. |
|
|
29
|
+
| `pre logout` | Removes the key from `.pre/config.json`. The key keeps working until an admin revokes it, so the output names its id. |
|
|
30
|
+
| `pre init --api-key <key> [--api-url <url>]` | Writes a key you already have to `.pre/config.json`. The key can also come from `PRE_API_KEY`. |
|
|
31
|
+
| `pre connect <db>` | Describes a SQLite or Postgres database: tables, row counts, columns, types, null share, sample values and foreign keys. It opens the database read-only. |
|
|
32
|
+
| `pre setup claude [--yes] [--dry-run] [--local]` | For Claude Code. Run it in your own terminal. It adds the API host and a remote Postgres host to Claude Code's sandbox network allowlist. It shows the change and asks first. It refuses to write when an agent runs it. See [Sandboxes](#sandboxes). |
|
|
33
|
+
| `pre status` | Checks that the API answers and accepts the key, and names the workspace. Lists the datasets and models this folder made. |
|
|
34
|
+
| `pre outcome define --name <slug> --sql <query> [--case account --cases accounts --event churn --event-past churned]` | Runs the outcome query and stores it. The query returns one row per case: the case id, then a 0/1 label. It refuses no rows, a label that is not 0 or 1, a repeated case id and a label that is always the same. The words go into every sentence the CLI prints. It guesses each word you leave out from the column names and says so. |
|
|
35
|
+
| `pre outcome show` | Lists the stored outcomes and their queries. |
|
|
36
|
+
| `pre extract --outcome <slug> --features <query>` | Runs the features query (the case id, then the features), joins it to the label and writes `.pre/data/<slug>.csv`. Nothing is sent. |
|
|
37
|
+
| `pre extract --outcome <slug> --upload [--refresh]` | Sends the CSV the dry run wrote, unchanged. With `--refresh`, or with no CSV yet, it runs the queries again first. |
|
|
38
|
+
| `pre train --outcome <slug> [--max-rules N] [--max-conditions N] [--no-wait] [--timeout S]` | Trains on the outcome's uploaded dataset. Every column but `id` is an input. It waits until the model is ready or failed and prints progress to stderr. When ready, it says how well the model predicts on held-out cases: the AUC and how many times the average rate the top 10% hold. |
|
|
39
|
+
| `pre rules --outcome <slug>` or `pre rules --model <id>` | Prints the rules as sentences, strongest first, with points and direction. The baseline shows as the starting rate. `--json` gives the API's rules. |
|
|
40
|
+
| `pre predict --outcome <slug> [--features <query>]` | Scores cases with the outcome's model. The query returns the case id, then the model inputs as of today, for example for the accounts still active. Without `--features` it scores the training cases as of their snapshot date and says so. It writes `.pre/data/<slug>.scores.csv`: the case id, the score, the points and the rules that fired as sentences, highest score first. It also writes `.pre/data/<slug>.scores.json` for `pre report`: the same scores with the rule ids that fired. It refuses a query that lacks an input or repeats a case id. |
|
|
41
|
+
| `pre report --outcome <slug> [--actions <file.json>] [--labels <file.json>] [--top N]` | Writes `.pre/reports/<slug>.html` and `.pre/reports/<slug>.csv` from the scores `pre predict` saved. It sends nothing. The page says how well the model predicts, lists the top rules as sentences and the `N` cases most likely to have the outcome, with their reasons. The CSV holds the same cases. `--actions` maps a rule id to one suggested action. `--labels` maps an input column to a plain label. It refuses a rule id or a column the model lacks. When the scores came from the training cases, the page says so. |
|
|
42
|
+
|
|
43
|
+
`<db>` is a SQLite file path, `sqlite:///path`, or `postgresql://user:password@host:5432/db`. Use `--schema` for
|
|
44
|
+
a Postgres schema other than `public`.
|
|
45
|
+
|
|
46
|
+
A `<query>` is the SQL itself or a path that ends in `.sql`. It must be one SELECT or WITH statement. The CLI
|
|
47
|
+
opens SQLite with `mode=ro` and runs Postgres in a read-only transaction, so a query cannot change data. For
|
|
48
|
+
Postgres, a database user with read access only is safer still.
|
|
49
|
+
|
|
50
|
+
## What leaves the machine
|
|
51
|
+
|
|
52
|
+
The CSV that `pre extract --upload` sends, and the rows that `pre predict` sends. Before it writes the CSV, `pre extract`:
|
|
53
|
+
|
|
54
|
+
1. Drops a column when every word of its name is an identifier word and one of them is a core word. Core words
|
|
55
|
+
are `email`, `phone`, `mobile`, `ip`, `address`, `street`, `ssn`, `name` and `username`. The other identifier
|
|
56
|
+
words are `mail`, `number`, `first`, `last`, `full`, `user`, `contact`, `home` and `work`. So `work_email` and
|
|
57
|
+
`first_name` go, and `plan_name` and `address_verified` stay.
|
|
58
|
+
2. Drops a column when a text value holds an email address or looks like a phone number.
|
|
59
|
+
3. Replaces the case id with an HMAC of it in a column named `id`. The key is in `.pre/secret` (mode 600).
|
|
60
|
+
`.pre/data/<slug>.ids.csv` maps each hash back to the case id. It stays on this machine.
|
|
61
|
+
|
|
62
|
+
The output names each dropped column and the reason.
|
|
63
|
+
|
|
64
|
+
`pre predict` sends each case as the same HMAC of its id and the model's inputs, in batches of 1,000. It sends no
|
|
65
|
+
other column. It maps the hashes back to the case ids in memory.
|
|
66
|
+
|
|
67
|
+
## Sandboxes
|
|
68
|
+
|
|
69
|
+
`pre setup` writes the smallest change it can. It merges into the file, so a second run changes nothing.
|
|
70
|
+
|
|
71
|
+
It writes `.claude/settings.json`, key `sandbox.network.allowedDomains`. For a Postgres on this machine, `--local`
|
|
72
|
+
also sets `sandbox.network.allowLocalBinding`, which opens every local port. It never sets `allowAllUnixSockets`.
|
|
73
|
+
Claude Code is the only agent at launch; others are on the roadmap. The onboard skill comes with the plugin
|
|
74
|
+
(`tools/agent-plugin`), not with `pre setup`.
|
|
75
|
+
|
|
76
|
+
These key names come from secondary sources. They live in `pre_cli/sandbox.py`.
|
|
77
|
+
|
|
78
|
+
## Local config
|
|
79
|
+
|
|
80
|
+
`.pre/config.json` holds the API URL, the key and its id, the database URL, the outcome queries and the dataset
|
|
81
|
+
and model ids that later commands create. While a `pre login --no-wait` waits, it also holds that sign-in. The API
|
|
82
|
+
can also list every dataset and model in the workspace. Each command that writes the config also writes
|
|
83
|
+
`.pre/.gitignore`, so git ignores the folder. Set `PRE_DIR` to use another folder.
|
|
84
|
+
|
|
85
|
+
## Tests
|
|
86
|
+
|
|
87
|
+
`scripts/check pre-cli` runs ruff and pytest. The `pre status` tests start a Prism mock of
|
|
88
|
+
`services/pre-api/openapi.yaml` with `npx`. They skip when `npx` is missing.
|
|
89
|
+
|
|
90
|
+
`tests/test_package.py` builds the sdist, a wheel from that sdist and a wheel from this folder. It fails if one
|
|
91
|
+
holds a file outside `pre_cli` and the three bundled packages, a `services`, `apps` or `ground_truth` path, a
|
|
92
|
+
`seed.py` or `check.py`, or the name of the PRE library. PRE must never reach PyPI.
|
|
93
|
+
|
|
94
|
+
## Release
|
|
95
|
+
|
|
96
|
+
The CLI is on PyPI as `stratyfy-cli`. The command it installs is `pre`. `.github/workflows/release-cli.yml`
|
|
97
|
+
publishes it with trusted publishing, so no PyPI token is stored.
|
|
98
|
+
|
|
99
|
+
1. Set the new `version` in `pyproject.toml`. It is the only place that holds the version.
|
|
100
|
+
2. Merge that change to `master`. CI runs `scripts/check pre-cli`, which builds the package and checks its files.
|
|
101
|
+
3. Tag the merge commit and push the tag: `git tag stratyfy-cli-v<version>`, then
|
|
102
|
+
`git push origin stratyfy-cli-v<version>`.
|
|
103
|
+
4. The workflow checks that the tag matches the version, builds, installs the wheel and runs `pre --version`. Then
|
|
104
|
+
it publishes in the GitHub environment `pypi`.
|
|
105
|
+
|
|
106
|
+
Users install it with Python 3.11 or later: `pipx install stratyfy-cli`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Bundle the three sibling packages into the sdist and the wheel, so `pipx install` needs nothing from this repo.
|
|
2
|
+
|
|
3
|
+
In the repo they live in `packages/`. An sdist holds them next to `pre_cli`, so a wheel built from it finds them.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
9
|
+
|
|
10
|
+
PACKAGES = Path(__file__).resolve().parents[2] / "packages"
|
|
11
|
+
BUNDLED = {"pre_api_client": "pre-api-client", "pre_words": "pre-words", "pre_report": "pre-report"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CustomBuildHook(BuildHookInterface):
|
|
15
|
+
def initialize(self, version, build_data):
|
|
16
|
+
# hatchling puts the repo's .gitignore in the sdist. It is not part of the CLI.
|
|
17
|
+
build_data["force_include"] = {k: v for k, v in build_data["force_include"].items() if v != ".gitignore"}
|
|
18
|
+
for name, folder in BUNDLED.items():
|
|
19
|
+
if not (Path(self.root) / name).is_dir():
|
|
20
|
+
build_data["force_include"][str(PACKAGES / folder / name)] = name
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains methods for accessing the API"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains endpoint functions for accessing the API"""
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from ... import errors
|
|
7
|
+
from ...client import AuthenticatedClient, Client
|
|
8
|
+
from ...models.create_dataset_body import CreateDatasetBody
|
|
9
|
+
from ...models.dataset import Dataset
|
|
10
|
+
from ...models.error import Error
|
|
11
|
+
from ...types import Response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_kwargs(
|
|
15
|
+
*,
|
|
16
|
+
body: CreateDatasetBody,
|
|
17
|
+
) -> dict[str, Any]:
|
|
18
|
+
headers: dict[str, Any] = {}
|
|
19
|
+
|
|
20
|
+
_kwargs: dict[str, Any] = {
|
|
21
|
+
"method": "post",
|
|
22
|
+
"url": "/datasets",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_kwargs["files"] = body.to_multipart()
|
|
26
|
+
|
|
27
|
+
headers["Content-Type"] = "multipart/form-data; boundary=+++"
|
|
28
|
+
|
|
29
|
+
_kwargs["headers"] = headers
|
|
30
|
+
return _kwargs
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _parse_response(
|
|
34
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
35
|
+
) -> Dataset | Error | None:
|
|
36
|
+
if response.status_code == 201:
|
|
37
|
+
response_201 = Dataset.from_dict(response.json())
|
|
38
|
+
|
|
39
|
+
return response_201
|
|
40
|
+
|
|
41
|
+
if response.status_code == 400:
|
|
42
|
+
response_400 = Error.from_dict(response.json())
|
|
43
|
+
|
|
44
|
+
return response_400
|
|
45
|
+
|
|
46
|
+
if response.status_code == 401:
|
|
47
|
+
response_401 = Error.from_dict(response.json())
|
|
48
|
+
|
|
49
|
+
return response_401
|
|
50
|
+
|
|
51
|
+
if response.status_code == 413:
|
|
52
|
+
response_413 = Error.from_dict(response.json())
|
|
53
|
+
|
|
54
|
+
return response_413
|
|
55
|
+
|
|
56
|
+
if client.raise_on_unexpected_status:
|
|
57
|
+
raise errors.UnexpectedStatus(response.status_code, response.content)
|
|
58
|
+
else:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _build_response(
|
|
63
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
64
|
+
) -> Response[Dataset | Error]:
|
|
65
|
+
return Response(
|
|
66
|
+
status_code=HTTPStatus(response.status_code),
|
|
67
|
+
content=response.content,
|
|
68
|
+
headers=response.headers,
|
|
69
|
+
parsed=_parse_response(client=client, response=response),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def sync_detailed(
|
|
74
|
+
*,
|
|
75
|
+
client: AuthenticatedClient,
|
|
76
|
+
body: CreateDatasetBody,
|
|
77
|
+
) -> Response[Dataset | Error]:
|
|
78
|
+
"""Upload labelled training data as a CSV or parquet file. The response holds the profile.
|
|
79
|
+
|
|
80
|
+
The API takes a file only. It does not take rows as JSON.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
body (CreateDatasetBody):
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
87
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Response[Dataset | Error]
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
kwargs = _get_kwargs(
|
|
94
|
+
body=body,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
response = client.get_httpx_client().request(
|
|
98
|
+
**kwargs,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return _build_response(client=client, response=response)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def sync(
|
|
105
|
+
*,
|
|
106
|
+
client: AuthenticatedClient,
|
|
107
|
+
body: CreateDatasetBody,
|
|
108
|
+
) -> Dataset | Error | None:
|
|
109
|
+
"""Upload labelled training data as a CSV or parquet file. The response holds the profile.
|
|
110
|
+
|
|
111
|
+
The API takes a file only. It does not take rows as JSON.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
body (CreateDatasetBody):
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
118
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dataset | Error
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
return sync_detailed(
|
|
125
|
+
client=client,
|
|
126
|
+
body=body,
|
|
127
|
+
).parsed
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def asyncio_detailed(
|
|
131
|
+
*,
|
|
132
|
+
client: AuthenticatedClient,
|
|
133
|
+
body: CreateDatasetBody,
|
|
134
|
+
) -> Response[Dataset | Error]:
|
|
135
|
+
"""Upload labelled training data as a CSV or parquet file. The response holds the profile.
|
|
136
|
+
|
|
137
|
+
The API takes a file only. It does not take rows as JSON.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
body (CreateDatasetBody):
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
144
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Response[Dataset | Error]
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
kwargs = _get_kwargs(
|
|
151
|
+
body=body,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
response = await client.get_async_httpx_client().request(**kwargs)
|
|
155
|
+
|
|
156
|
+
return _build_response(client=client, response=response)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def asyncio(
|
|
160
|
+
*,
|
|
161
|
+
client: AuthenticatedClient,
|
|
162
|
+
body: CreateDatasetBody,
|
|
163
|
+
) -> Dataset | Error | None:
|
|
164
|
+
"""Upload labelled training data as a CSV or parquet file. The response holds the profile.
|
|
165
|
+
|
|
166
|
+
The API takes a file only. It does not take rows as JSON.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
body (CreateDatasetBody):
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
173
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dataset | Error
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
await asyncio_detailed(
|
|
181
|
+
client=client,
|
|
182
|
+
body=body,
|
|
183
|
+
)
|
|
184
|
+
).parsed
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import quote
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from ... import errors
|
|
8
|
+
from ...client import AuthenticatedClient, Client
|
|
9
|
+
from ...models.create_model_version_body import CreateModelVersionBody
|
|
10
|
+
from ...models.error import Error
|
|
11
|
+
from ...models.model import Model
|
|
12
|
+
from ...types import Response
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_kwargs(
|
|
16
|
+
model_id: str,
|
|
17
|
+
*,
|
|
18
|
+
body: CreateModelVersionBody,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
headers: dict[str, Any] = {}
|
|
21
|
+
|
|
22
|
+
_kwargs: dict[str, Any] = {
|
|
23
|
+
"method": "post",
|
|
24
|
+
"url": "/models/{model_id}/versions".format(
|
|
25
|
+
model_id=quote(str(model_id), safe=""),
|
|
26
|
+
),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_kwargs["json"] = body.to_dict()
|
|
30
|
+
|
|
31
|
+
headers["Content-Type"] = "application/json"
|
|
32
|
+
|
|
33
|
+
_kwargs["headers"] = headers
|
|
34
|
+
return _kwargs
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_response(
|
|
38
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
39
|
+
) -> Error | Model | None:
|
|
40
|
+
if response.status_code == 202:
|
|
41
|
+
response_202 = Model.from_dict(response.json())
|
|
42
|
+
|
|
43
|
+
return response_202
|
|
44
|
+
|
|
45
|
+
if response.status_code == 400:
|
|
46
|
+
response_400 = Error.from_dict(response.json())
|
|
47
|
+
|
|
48
|
+
return response_400
|
|
49
|
+
|
|
50
|
+
if response.status_code == 401:
|
|
51
|
+
response_401 = Error.from_dict(response.json())
|
|
52
|
+
|
|
53
|
+
return response_401
|
|
54
|
+
|
|
55
|
+
if response.status_code == 404:
|
|
56
|
+
response_404 = Error.from_dict(response.json())
|
|
57
|
+
|
|
58
|
+
return response_404
|
|
59
|
+
|
|
60
|
+
if response.status_code == 409:
|
|
61
|
+
response_409 = Error.from_dict(response.json())
|
|
62
|
+
|
|
63
|
+
return response_409
|
|
64
|
+
|
|
65
|
+
if client.raise_on_unexpected_status:
|
|
66
|
+
raise errors.UnexpectedStatus(response.status_code, response.content)
|
|
67
|
+
else:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _build_response(
|
|
72
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
73
|
+
) -> Response[Error | Model]:
|
|
74
|
+
return Response(
|
|
75
|
+
status_code=HTTPStatus(response.status_code),
|
|
76
|
+
content=response.content,
|
|
77
|
+
headers=response.headers,
|
|
78
|
+
parsed=_parse_response(client=client, response=response),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def sync_detailed(
|
|
83
|
+
model_id: str,
|
|
84
|
+
*,
|
|
85
|
+
client: AuthenticatedClient,
|
|
86
|
+
body: CreateModelVersionBody,
|
|
87
|
+
) -> Response[Error | Model]:
|
|
88
|
+
"""Add, edit or remove rules. This makes a new version and refits it. The old version does not change.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
model_id (str):
|
|
92
|
+
body (CreateModelVersionBody):
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
96
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Response[Error | Model]
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
kwargs = _get_kwargs(
|
|
103
|
+
model_id=model_id,
|
|
104
|
+
body=body,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
response = client.get_httpx_client().request(
|
|
108
|
+
**kwargs,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return _build_response(client=client, response=response)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def sync(
|
|
115
|
+
model_id: str,
|
|
116
|
+
*,
|
|
117
|
+
client: AuthenticatedClient,
|
|
118
|
+
body: CreateModelVersionBody,
|
|
119
|
+
) -> Error | Model | None:
|
|
120
|
+
"""Add, edit or remove rules. This makes a new version and refits it. The old version does not change.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
model_id (str):
|
|
124
|
+
body (CreateModelVersionBody):
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
128
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Error | Model
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
return sync_detailed(
|
|
135
|
+
model_id=model_id,
|
|
136
|
+
client=client,
|
|
137
|
+
body=body,
|
|
138
|
+
).parsed
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def asyncio_detailed(
|
|
142
|
+
model_id: str,
|
|
143
|
+
*,
|
|
144
|
+
client: AuthenticatedClient,
|
|
145
|
+
body: CreateModelVersionBody,
|
|
146
|
+
) -> Response[Error | Model]:
|
|
147
|
+
"""Add, edit or remove rules. This makes a new version and refits it. The old version does not change.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
model_id (str):
|
|
151
|
+
body (CreateModelVersionBody):
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
155
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Response[Error | Model]
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
kwargs = _get_kwargs(
|
|
162
|
+
model_id=model_id,
|
|
163
|
+
body=body,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
response = await client.get_async_httpx_client().request(**kwargs)
|
|
167
|
+
|
|
168
|
+
return _build_response(client=client, response=response)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def asyncio(
|
|
172
|
+
model_id: str,
|
|
173
|
+
*,
|
|
174
|
+
client: AuthenticatedClient,
|
|
175
|
+
body: CreateModelVersionBody,
|
|
176
|
+
) -> Error | Model | None:
|
|
177
|
+
"""Add, edit or remove rules. This makes a new version and refits it. The old version does not change.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
model_id (str):
|
|
181
|
+
body (CreateModelVersionBody):
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
185
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Error | Model
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
await asyncio_detailed(
|
|
193
|
+
model_id=model_id,
|
|
194
|
+
client=client,
|
|
195
|
+
body=body,
|
|
196
|
+
)
|
|
197
|
+
).parsed
|