typedstandards 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.
- typedstandards-0.1.0/.gitignore +10 -0
- typedstandards-0.1.0/CHANGELOG.md +28 -0
- typedstandards-0.1.0/LICENSE +21 -0
- typedstandards-0.1.0/PKG-INFO +231 -0
- typedstandards-0.1.0/README.md +207 -0
- typedstandards-0.1.0/hatch_build.py +62 -0
- typedstandards-0.1.0/package-lock.json +96 -0
- typedstandards-0.1.0/package.json +9 -0
- typedstandards-0.1.0/pyproject.toml +89 -0
- typedstandards-0.1.0/scripts/publish.sh +201 -0
- typedstandards-0.1.0/scripts/smoke_wheel.py +109 -0
- typedstandards-0.1.0/src/typedstandards/__init__.py +67 -0
- typedstandards-0.1.0/src/typedstandards/_badge.py +167 -0
- typedstandards-0.1.0/src/typedstandards/_cli.py +74 -0
- typedstandards-0.1.0/src/typedstandards/_commands.py +156 -0
- typedstandards-0.1.0/src/typedstandards/_comparison.py +164 -0
- typedstandards-0.1.0/src/typedstandards/_node.py +68 -0
- typedstandards-0.1.0/src/typedstandards/_notebook.py +200 -0
- typedstandards-0.1.0/src/typedstandards/_show.py +381 -0
- typedstandards-0.1.0/src/typedstandards/_sidecar.py +102 -0
- typedstandards-0.1.0/src/typedstandards/errors.py +58 -0
- typedstandards-0.1.0/src/typedstandards/pin.py +156 -0
- typedstandards-0.1.0/tests/conftest.py +69 -0
- typedstandards-0.1.0/tests/fixtures/README.md +76 -0
- typedstandards-0.1.0/tests/fixtures/badge-golden.json +34 -0
- typedstandards-0.1.0/tests/fixtures/capture_records.py +100 -0
- typedstandards-0.1.0/tests/fixtures/first-note.bundle.json +78 -0
- typedstandards-0.1.0/tests/fixtures/record-active.bundle.json +79 -0
- typedstandards-0.1.0/tests/fixtures/record-active.signed.json +62 -0
- typedstandards-0.1.0/tests/fixtures/record-active.verify.json +74 -0
- typedstandards-0.1.0/tests/fixtures/record-withdrawn.bundle.json +116 -0
- typedstandards-0.1.0/tests/fixtures/record-withdrawn.signed.json +62 -0
- typedstandards-0.1.0/tests/fixtures/record-withdrawn.verify.json +120 -0
- typedstandards-0.1.0/tests/fixtures/record-withdrawn.withdrawal.json +30 -0
- typedstandards-0.1.0/tests/fixtures/reference-golden.json +1151 -0
- typedstandards-0.1.0/tests/fixtures/show-active.html +1 -0
- typedstandards-0.1.0/tests/fixtures/show-withdrawn.html +1 -0
- typedstandards-0.1.0/tests/guards.py +176 -0
- typedstandards-0.1.0/tests/support.py +147 -0
- typedstandards-0.1.0/tests/test_badge.py +332 -0
- typedstandards-0.1.0/tests/test_commands.py +181 -0
- typedstandards-0.1.0/tests/test_comparison.py +225 -0
- typedstandards-0.1.0/tests/test_d9.py +89 -0
- typedstandards-0.1.0/tests/test_exit_codes.py +95 -0
- typedstandards-0.1.0/tests/test_fixtures.py +36 -0
- typedstandards-0.1.0/tests/test_golden.py +86 -0
- typedstandards-0.1.0/tests/test_guards.py +266 -0
- typedstandards-0.1.0/tests/test_node.py +112 -0
- typedstandards-0.1.0/tests/test_offline.py +45 -0
- typedstandards-0.1.0/tests/test_pin.py +200 -0
- typedstandards-0.1.0/tests/test_readme.py +71 -0
- typedstandards-0.1.0/tests/test_show.py +270 -0
- typedstandards-0.1.0/tests/test_sidecar.py +117 -0
- typedstandards-0.1.0/tests/test_version.py +41 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-10-04
|
|
4
|
+
|
|
5
|
+
- `sign`, `withdraw`, `attest`, `view` and `verify`: pass-throughs to `@typedstandards/cli` 0.2.0,
|
|
6
|
+
vendored into the wheel at build time and run as a child process with the inherited environment.
|
|
7
|
+
Each returns the CLI's stdout parsed as JSON.
|
|
8
|
+
- A Node locator: `TYPEDSTANDARDS_NODE`, then `node` on `PATH`; floor 20.19.0.
|
|
9
|
+
- Exit codes 1 to 4 raise `VerificationError`, `UsageError`, `SeedError` and `InternalError`, under
|
|
10
|
+
`CliError`; a missing or old Node raises `NodeLocatorError`.
|
|
11
|
+
- `verify` drops a bundle's top-level `trustRegistry` before the CLI sees it (typedstandards#136).
|
|
12
|
+
- `CLI_VERSION = "0.2.0"` and `cli_version()`.
|
|
13
|
+
- `pin(url)`: fetches once and returns the bytes with a `queries[]` retrieval entry (`url`,
|
|
14
|
+
`sha256`, `bytes`, `httpStatus`, `fetchedAt`; `rowsUpdatedAt` and `datasetId` for a portal
|
|
15
|
+
resource); `save=` writes both.
|
|
16
|
+
- `badge_cell`: the verifier badge as a notebook's first cell, or a `mo.md` cell's source; no hash
|
|
17
|
+
and no time in the cell.
|
|
18
|
+
- `comparison_cell`: the spec §8.7.4 comparison cell, appended as the last cell; values must be
|
|
19
|
+
literals.
|
|
20
|
+
- `sidecar`: `<artifact file name>.record.yaml` from `view`'s output, without `package` and
|
|
21
|
+
`trustRegistry`.
|
|
22
|
+
- `show`: HTML for Jupyter (`_repr_html_`) or Marimo (`mo.Html`) from a record and its
|
|
23
|
+
`verify --json` result.
|
|
24
|
+
- `badge_cell` no longer reads a URL's host and port as a time: `https://192.168.1.10:8080/…` is
|
|
25
|
+
accepted. A date or time in the URL's path, query or fragment (as written or percent-encoded) or
|
|
26
|
+
in a fact is refused.
|
|
27
|
+
- README: only the calls that run the CLI need Node (the five commands, `cli_version()`, and
|
|
28
|
+
`show` without a result); its links are absolute, so they resolve on the PyPI page.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nathan Storey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: typedstandards
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sign, withdraw, attest to, build views of and verify Typed Standards records from Python, through @typedstandards/cli as a child process.
|
|
5
|
+
Project-URL: Homepage, https://github.com/npstorey/typedstandards-python
|
|
6
|
+
Project-URL: Issues, https://github.com/npstorey/typedstandards-python/issues
|
|
7
|
+
Author: Nathan Storey
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: notebook,provenance,signing,typed-standards,verification
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: httpx>=0.28
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# typedstandards
|
|
26
|
+
|
|
27
|
+
Sign, withdraw, attest to, build views of and verify [Typed Standards](https://typedstandards.org)
|
|
28
|
+
records from Python. The package drives
|
|
29
|
+
[`@typedstandards/cli`](https://www.npmjs.com/package/@typedstandards/cli) 0.2.0 as a child
|
|
30
|
+
process. It holds no key, reads no signing seed, and computes none of the format's hashes: the CLI
|
|
31
|
+
does all of the format's work.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install typedstandards # or: uv add typedstandards
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The wheel carries the CLI and its dependencies (vendored at build time from this repository's
|
|
40
|
+
`package-lock.json`), so installing needs nothing from npm. Running needs Node.js: the wrapper and
|
|
41
|
+
the CLI need Node 20.19 or later, and a `@typedstandards/host-core` site build needs Node 22 or
|
|
42
|
+
later.
|
|
43
|
+
|
|
44
|
+
The wrapper looks for Node in this order:
|
|
45
|
+
|
|
46
|
+
1. `TYPEDSTANDARDS_NODE`, when set: the path (or name on `PATH`) of a Node binary;
|
|
47
|
+
2. `node` on `PATH`.
|
|
48
|
+
|
|
49
|
+
Only the calls that run the CLI need Node: `sign`, `withdraw`, `attest`, `view`, `verify`,
|
|
50
|
+
`cli_version()`, and `show` without a precomputed result. With no Node, or one older than 20.19.0,
|
|
51
|
+
each of those raises `typedstandards.NodeLocatorError`, whose message names the floor and
|
|
52
|
+
`TYPEDSTANDARDS_NODE`. `pin`, `badge_cell`, `comparison_cell`, `sidecar` and
|
|
53
|
+
`show(record, result)` run without Node.
|
|
54
|
+
|
|
55
|
+
Linux and macOS are tested. Windows is untested.
|
|
56
|
+
|
|
57
|
+
## The signing key
|
|
58
|
+
|
|
59
|
+
The CLI reads the signing seed (the standard base64 of a 32-byte Ed25519 seed) from one environment
|
|
60
|
+
variable, `TYPEDSTANDARDS_SIGNING_SEED_B64`, and from nowhere else. The wrapper never reads it: it
|
|
61
|
+
starts the CLI with the environment it inherited, and passes no environment of its own. Set the
|
|
62
|
+
variable from a secret store for the process that signs, for example:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
op run --env-file=signing.env -- jupyter lab
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
where `signing.env` maps `TYPEDSTANDARDS_SIGNING_SEED_B64` to a secret reference. Make a new seed
|
|
69
|
+
with:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
openssl rand -base64 32
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Only `sign`, `withdraw` and `attest` need it; `view` and `verify` do not.
|
|
76
|
+
|
|
77
|
+
## Use
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import typedstandards as ts
|
|
81
|
+
|
|
82
|
+
signed = ts.sign(
|
|
83
|
+
{
|
|
84
|
+
"type": "content/analysis/v1",
|
|
85
|
+
"producerProfile": "scripted-recomputation/example",
|
|
86
|
+
"captureMethod": "script-run",
|
|
87
|
+
"prompt": "Recompute the summary table.",
|
|
88
|
+
"promptVisibility": "full_text",
|
|
89
|
+
"queries": [],
|
|
90
|
+
"dataSources": [],
|
|
91
|
+
"cost": {"model": "none"},
|
|
92
|
+
"skillMetadata": {},
|
|
93
|
+
"trace": {},
|
|
94
|
+
"signer": {"bindingTier": "pseudonymous", "displayName": "Example analyst"},
|
|
95
|
+
},
|
|
96
|
+
output_file="analysis.ipynb",
|
|
97
|
+
) # signed inline under raw-bytes/v1
|
|
98
|
+
|
|
99
|
+
bundle = ts.view(signed, visibility="public", title="Example analysis")
|
|
100
|
+
result = ts.verify(bundle) # {ok, nodeId, failures, checks, lifecycle}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Each function returns the CLI's stdout parsed as JSON. An input may be a mapping (sent to the CLI
|
|
104
|
+
as JSON on standard input) or the path of a JSON file.
|
|
105
|
+
|
|
106
|
+
| Function | CLI command | Returns |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `sign(input, *, output_file=None, output_url=None, content_type=None)` | `sign` | `{package, envelopeHash, signature}` |
|
|
109
|
+
| `withdraw(input)` | `withdraw` | `{node, nodeId, signature}` |
|
|
110
|
+
| `attest(input)` | `attest` | `{node, nodeId, signature}` |
|
|
111
|
+
| `view(signed, *, visibility, attestations=(), trust_registry_url=None, package_url=None, title=None)` | `view` | the commitment view, package inline |
|
|
112
|
+
| `verify(input, *, blobs=(), full=True)` | `verify` (`--json` when `full`) | `{ok, nodeId, failures, checks, lifecycle}` |
|
|
113
|
+
|
|
114
|
+
`view` writes the mappings it is given to temporary files, removed before it returns. The CLI's
|
|
115
|
+
[README](https://github.com/npstorey/typedstandards/tree/main/packages/cli#readme) describes each
|
|
116
|
+
command's inputs. What the CLI prints on stderr when it succeeds (attention readings, such as an
|
|
117
|
+
offline `registry_unavailable`) is logged at INFO on the `typedstandards` logger.
|
|
118
|
+
|
|
119
|
+
`typedstandards.CLI_VERSION` is the version of the vendored CLI (`"0.2.0"`), and
|
|
120
|
+
`typedstandards.cli_version()` asks the vendored CLI for it.
|
|
121
|
+
|
|
122
|
+
### Errors
|
|
123
|
+
|
|
124
|
+
A non-zero exit raises a subclass of `typedstandards.CliError`, which carries `exit_code`, `stderr`
|
|
125
|
+
(the CLI's standard error) and `command`:
|
|
126
|
+
|
|
127
|
+
| Exit | Exception | Meaning |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| 1 | `VerificationError` | a record, or the CLI's own result, did not verify; for `verify`, `.document` is the `{ok: false, ...}` verdict |
|
|
130
|
+
| 2 | `UsageError` | an argument or an input is wrong |
|
|
131
|
+
| 3 | `SeedError` | the seed's variable is missing or malformed |
|
|
132
|
+
| 4 | `InternalError` | an internal error in the CLI |
|
|
133
|
+
|
|
134
|
+
### Verifying a served bundle
|
|
135
|
+
|
|
136
|
+
`@typedstandards/host-core` inlines a top-level `trustRegistry` in every bundle it serves under a
|
|
137
|
+
registry, and CLI 0.2.0's `verify` refuses that key
|
|
138
|
+
([typedstandards#136](https://github.com/npstorey/typedstandards/issues/136)). Until the wrapper
|
|
139
|
+
pins a CLI that accepts it, `verify` drops a bundle's top-level `trustRegistry` before the CLI
|
|
140
|
+
sees the bundle, and changes nothing else. A test pins that the CLI receives the same document
|
|
141
|
+
minus that one key.
|
|
142
|
+
|
|
143
|
+
## Notebook helpers
|
|
144
|
+
|
|
145
|
+
Five helpers arrange and render around the CLI. None of them computes one of the format's hashes,
|
|
146
|
+
reads the seed, or verifies anything itself.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import io
|
|
150
|
+
import pandas as pd
|
|
151
|
+
import typedstandards as ts
|
|
152
|
+
|
|
153
|
+
# Before signing: pin each input, and add the reader's badge and the comparison cell.
|
|
154
|
+
content, entry = ts.pin("https://data.example.org/resource/abcd-1234.csv", licence="CC-BY-4.0")
|
|
155
|
+
frame = pd.read_csv(io.BytesIO(content))
|
|
156
|
+
|
|
157
|
+
ts.badge_cell(
|
|
158
|
+
"https://records.example.org/bundles/analysis.bundle.json",
|
|
159
|
+
capture_method="script-run",
|
|
160
|
+
notebook="analysis.ipynb",
|
|
161
|
+
)
|
|
162
|
+
ts.comparison_cell(
|
|
163
|
+
"analysis.ipynb",
|
|
164
|
+
{"rows": 1204, "mean_fare": 13.75},
|
|
165
|
+
recompute="recompute_key_metrics()",
|
|
166
|
+
captured_at="2026-10-03T12:00:00Z",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Sign (record is an envelope input like the one under Use), then serve and show.
|
|
170
|
+
signed = ts.sign({**record, "queries": [entry]}, output_file="analysis.ipynb")
|
|
171
|
+
bundle = ts.view(signed, visibility="public", title="Example analysis")
|
|
172
|
+
ts.sidecar(bundle, "analysis.ipynb") # writes analysis.ipynb.record.yaml
|
|
173
|
+
ts.show(bundle) # in Jupyter; ts.show(bundle, marimo=True) in Marimo
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- **`pin(url, *, licence=None, dataset_id=None, portal_metadata=None, save=None, ...)`** fetches
|
|
177
|
+
`url` once and returns `Pinned(content, entry)`: the response body, and a retrieval entry for
|
|
178
|
+
the record's `queries[]` with `url`, `sha256` (of the body), `bytes`, `httpStatus` and
|
|
179
|
+
`fetchedAt` (ISO 8601 UTC) under `arguments`. A URL shaped like an open-data portal resource
|
|
180
|
+
(`…/resource/<id>[.ext]` or `…/api/views/<id>/rows.<ext>`, `<id>` being `xxxx-xxxx`) gets a
|
|
181
|
+
second request to `<origin>/api/views/<id>`, and the entry gains the portal's `rowsUpdatedAt`
|
|
182
|
+
(epoch seconds) and `datasetId`. A response that is not 2xx raises. `save=` writes the bytes
|
|
183
|
+
to that path and the entry to `<path>.pin.json`. The SHA-256 is the one digest the package
|
|
184
|
+
computes: a signed assertion in `queries[]` that no check recomputes.
|
|
185
|
+
- **`badge_cell(bundle_url, *, capture_method, notebook=None, host=None, marimo=False)`** writes
|
|
186
|
+
the verifier badge, linked to `https://typedstandards.org/verify?url=<the bundle URL,
|
|
187
|
+
percent-encoded>` as `@typedstandards/host-core` writes it, above a two-row table (the host and
|
|
188
|
+
the capture method). With `notebook`, it is inserted as the notebook's first cell (id
|
|
189
|
+
`typedstandards-badge` on nbformat 4.5). With `marimo=True`, it returns the source of a
|
|
190
|
+
`mo.md(...)` cell to paste into the app. The cell is written before signing and is part of the
|
|
191
|
+
signed bytes, so it names no hash and no time; a URL or value holding a 64-hex string, a date or
|
|
192
|
+
a time is refused. The URL's host and port are not read as a time (`192.168.1.10:8080` is
|
|
193
|
+
accepted).
|
|
194
|
+
- **`comparison_cell(notebook, values, *, recompute, captured_at)`** appends the comparison cell
|
|
195
|
+
of spec §8.7.4 as the last cell (id `typedstandards-comparison`): the values as Python literals,
|
|
196
|
+
`current = <recompute>`, and a loop that prints each delta. Values are `None`, `bool`, `int`,
|
|
197
|
+
finite `float`, `str`, and lists and str-keyed dicts of those; anything else is refused.
|
|
198
|
+
- **`sidecar(view, artifact, *, directory=None)`** writes the commitment view as YAML (spec
|
|
199
|
+
§8.8.3) beside the artifact: every field `view` printed except the inline `package` and a
|
|
200
|
+
served bundle's `trustRegistry`, which are not §8.8.1 fields. The file is named
|
|
201
|
+
`<artifact's file name>.record.yaml`, extension kept (`analysis.ipynb.record.yaml`): the spec's
|
|
202
|
+
`<artifact-basename>` does not say whether the extension stays, and keeping it is the POSIX
|
|
203
|
+
basename and cannot collide when two artifacts share a stem (`analysis.ipynb` beside
|
|
204
|
+
`analysis.py`).
|
|
205
|
+
- **`show(record, result=None, *, role_path=("role",), marimo=False)`** renders a record (what
|
|
206
|
+
`view` or `sign` printed) with its `verify --json` result: the type, the role, the signer,
|
|
207
|
+
the hash, `createdAt`, the `vcsRef` (marked as asserted and not fetched), the status with its
|
|
208
|
+
reason or successor, one line per check, and a sentence saying that verification does not say
|
|
209
|
+
the analysis is correct. Without `result` it runs `verify`. In Jupyter it returns an object
|
|
210
|
+
with `_repr_html_`; with `marimo=True`, `mo.Html`. The role is a signed assertion the signer
|
|
211
|
+
made, read from the package's `extensions` at `role_path` (by default `extensions["role"]`, a
|
|
212
|
+
string or a list of strings); `show` labels it as the signer's and checks nothing about it.
|
|
213
|
+
|
|
214
|
+
The notebook helpers edit the notebook as JSON, splicing the new cell into `cells` so every
|
|
215
|
+
other byte of the file stays as written. `httpx` is imported only inside `pin`, PyYAML only
|
|
216
|
+
inside `sidecar`, and `marimo` only inside a Marimo call; `IPython`, `marimo` and `nbformat`
|
|
217
|
+
are not dependencies.
|
|
218
|
+
|
|
219
|
+
## Versions
|
|
220
|
+
|
|
221
|
+
Each wrapper release pins one CLI version exactly. A CLI upgrade reaches users as a wrapper release
|
|
222
|
+
that moves the pin.
|
|
223
|
+
|
|
224
|
+
## Development
|
|
225
|
+
|
|
226
|
+
See [CLAUDE.md](https://github.com/npstorey/typedstandards-python/blob/main/CLAUDE.md) for the development loop and the checks CI runs.
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT. The wheel also carries each vendored npm package's own licence file: five are MIT, and
|
|
231
|
+
`canonicalize` is Apache-2.0.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# typedstandards
|
|
2
|
+
|
|
3
|
+
Sign, withdraw, attest to, build views of and verify [Typed Standards](https://typedstandards.org)
|
|
4
|
+
records from Python. The package drives
|
|
5
|
+
[`@typedstandards/cli`](https://www.npmjs.com/package/@typedstandards/cli) 0.2.0 as a child
|
|
6
|
+
process. It holds no key, reads no signing seed, and computes none of the format's hashes: the CLI
|
|
7
|
+
does all of the format's work.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install typedstandards # or: uv add typedstandards
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The wheel carries the CLI and its dependencies (vendored at build time from this repository's
|
|
16
|
+
`package-lock.json`), so installing needs nothing from npm. Running needs Node.js: the wrapper and
|
|
17
|
+
the CLI need Node 20.19 or later, and a `@typedstandards/host-core` site build needs Node 22 or
|
|
18
|
+
later.
|
|
19
|
+
|
|
20
|
+
The wrapper looks for Node in this order:
|
|
21
|
+
|
|
22
|
+
1. `TYPEDSTANDARDS_NODE`, when set: the path (or name on `PATH`) of a Node binary;
|
|
23
|
+
2. `node` on `PATH`.
|
|
24
|
+
|
|
25
|
+
Only the calls that run the CLI need Node: `sign`, `withdraw`, `attest`, `view`, `verify`,
|
|
26
|
+
`cli_version()`, and `show` without a precomputed result. With no Node, or one older than 20.19.0,
|
|
27
|
+
each of those raises `typedstandards.NodeLocatorError`, whose message names the floor and
|
|
28
|
+
`TYPEDSTANDARDS_NODE`. `pin`, `badge_cell`, `comparison_cell`, `sidecar` and
|
|
29
|
+
`show(record, result)` run without Node.
|
|
30
|
+
|
|
31
|
+
Linux and macOS are tested. Windows is untested.
|
|
32
|
+
|
|
33
|
+
## The signing key
|
|
34
|
+
|
|
35
|
+
The CLI reads the signing seed (the standard base64 of a 32-byte Ed25519 seed) from one environment
|
|
36
|
+
variable, `TYPEDSTANDARDS_SIGNING_SEED_B64`, and from nowhere else. The wrapper never reads it: it
|
|
37
|
+
starts the CLI with the environment it inherited, and passes no environment of its own. Set the
|
|
38
|
+
variable from a secret store for the process that signs, for example:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
op run --env-file=signing.env -- jupyter lab
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
where `signing.env` maps `TYPEDSTANDARDS_SIGNING_SEED_B64` to a secret reference. Make a new seed
|
|
45
|
+
with:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
openssl rand -base64 32
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Only `sign`, `withdraw` and `attest` need it; `view` and `verify` do not.
|
|
52
|
+
|
|
53
|
+
## Use
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import typedstandards as ts
|
|
57
|
+
|
|
58
|
+
signed = ts.sign(
|
|
59
|
+
{
|
|
60
|
+
"type": "content/analysis/v1",
|
|
61
|
+
"producerProfile": "scripted-recomputation/example",
|
|
62
|
+
"captureMethod": "script-run",
|
|
63
|
+
"prompt": "Recompute the summary table.",
|
|
64
|
+
"promptVisibility": "full_text",
|
|
65
|
+
"queries": [],
|
|
66
|
+
"dataSources": [],
|
|
67
|
+
"cost": {"model": "none"},
|
|
68
|
+
"skillMetadata": {},
|
|
69
|
+
"trace": {},
|
|
70
|
+
"signer": {"bindingTier": "pseudonymous", "displayName": "Example analyst"},
|
|
71
|
+
},
|
|
72
|
+
output_file="analysis.ipynb",
|
|
73
|
+
) # signed inline under raw-bytes/v1
|
|
74
|
+
|
|
75
|
+
bundle = ts.view(signed, visibility="public", title="Example analysis")
|
|
76
|
+
result = ts.verify(bundle) # {ok, nodeId, failures, checks, lifecycle}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Each function returns the CLI's stdout parsed as JSON. An input may be a mapping (sent to the CLI
|
|
80
|
+
as JSON on standard input) or the path of a JSON file.
|
|
81
|
+
|
|
82
|
+
| Function | CLI command | Returns |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `sign(input, *, output_file=None, output_url=None, content_type=None)` | `sign` | `{package, envelopeHash, signature}` |
|
|
85
|
+
| `withdraw(input)` | `withdraw` | `{node, nodeId, signature}` |
|
|
86
|
+
| `attest(input)` | `attest` | `{node, nodeId, signature}` |
|
|
87
|
+
| `view(signed, *, visibility, attestations=(), trust_registry_url=None, package_url=None, title=None)` | `view` | the commitment view, package inline |
|
|
88
|
+
| `verify(input, *, blobs=(), full=True)` | `verify` (`--json` when `full`) | `{ok, nodeId, failures, checks, lifecycle}` |
|
|
89
|
+
|
|
90
|
+
`view` writes the mappings it is given to temporary files, removed before it returns. The CLI's
|
|
91
|
+
[README](https://github.com/npstorey/typedstandards/tree/main/packages/cli#readme) describes each
|
|
92
|
+
command's inputs. What the CLI prints on stderr when it succeeds (attention readings, such as an
|
|
93
|
+
offline `registry_unavailable`) is logged at INFO on the `typedstandards` logger.
|
|
94
|
+
|
|
95
|
+
`typedstandards.CLI_VERSION` is the version of the vendored CLI (`"0.2.0"`), and
|
|
96
|
+
`typedstandards.cli_version()` asks the vendored CLI for it.
|
|
97
|
+
|
|
98
|
+
### Errors
|
|
99
|
+
|
|
100
|
+
A non-zero exit raises a subclass of `typedstandards.CliError`, which carries `exit_code`, `stderr`
|
|
101
|
+
(the CLI's standard error) and `command`:
|
|
102
|
+
|
|
103
|
+
| Exit | Exception | Meaning |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| 1 | `VerificationError` | a record, or the CLI's own result, did not verify; for `verify`, `.document` is the `{ok: false, ...}` verdict |
|
|
106
|
+
| 2 | `UsageError` | an argument or an input is wrong |
|
|
107
|
+
| 3 | `SeedError` | the seed's variable is missing or malformed |
|
|
108
|
+
| 4 | `InternalError` | an internal error in the CLI |
|
|
109
|
+
|
|
110
|
+
### Verifying a served bundle
|
|
111
|
+
|
|
112
|
+
`@typedstandards/host-core` inlines a top-level `trustRegistry` in every bundle it serves under a
|
|
113
|
+
registry, and CLI 0.2.0's `verify` refuses that key
|
|
114
|
+
([typedstandards#136](https://github.com/npstorey/typedstandards/issues/136)). Until the wrapper
|
|
115
|
+
pins a CLI that accepts it, `verify` drops a bundle's top-level `trustRegistry` before the CLI
|
|
116
|
+
sees the bundle, and changes nothing else. A test pins that the CLI receives the same document
|
|
117
|
+
minus that one key.
|
|
118
|
+
|
|
119
|
+
## Notebook helpers
|
|
120
|
+
|
|
121
|
+
Five helpers arrange and render around the CLI. None of them computes one of the format's hashes,
|
|
122
|
+
reads the seed, or verifies anything itself.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import io
|
|
126
|
+
import pandas as pd
|
|
127
|
+
import typedstandards as ts
|
|
128
|
+
|
|
129
|
+
# Before signing: pin each input, and add the reader's badge and the comparison cell.
|
|
130
|
+
content, entry = ts.pin("https://data.example.org/resource/abcd-1234.csv", licence="CC-BY-4.0")
|
|
131
|
+
frame = pd.read_csv(io.BytesIO(content))
|
|
132
|
+
|
|
133
|
+
ts.badge_cell(
|
|
134
|
+
"https://records.example.org/bundles/analysis.bundle.json",
|
|
135
|
+
capture_method="script-run",
|
|
136
|
+
notebook="analysis.ipynb",
|
|
137
|
+
)
|
|
138
|
+
ts.comparison_cell(
|
|
139
|
+
"analysis.ipynb",
|
|
140
|
+
{"rows": 1204, "mean_fare": 13.75},
|
|
141
|
+
recompute="recompute_key_metrics()",
|
|
142
|
+
captured_at="2026-10-03T12:00:00Z",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Sign (record is an envelope input like the one under Use), then serve and show.
|
|
146
|
+
signed = ts.sign({**record, "queries": [entry]}, output_file="analysis.ipynb")
|
|
147
|
+
bundle = ts.view(signed, visibility="public", title="Example analysis")
|
|
148
|
+
ts.sidecar(bundle, "analysis.ipynb") # writes analysis.ipynb.record.yaml
|
|
149
|
+
ts.show(bundle) # in Jupyter; ts.show(bundle, marimo=True) in Marimo
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **`pin(url, *, licence=None, dataset_id=None, portal_metadata=None, save=None, ...)`** fetches
|
|
153
|
+
`url` once and returns `Pinned(content, entry)`: the response body, and a retrieval entry for
|
|
154
|
+
the record's `queries[]` with `url`, `sha256` (of the body), `bytes`, `httpStatus` and
|
|
155
|
+
`fetchedAt` (ISO 8601 UTC) under `arguments`. A URL shaped like an open-data portal resource
|
|
156
|
+
(`…/resource/<id>[.ext]` or `…/api/views/<id>/rows.<ext>`, `<id>` being `xxxx-xxxx`) gets a
|
|
157
|
+
second request to `<origin>/api/views/<id>`, and the entry gains the portal's `rowsUpdatedAt`
|
|
158
|
+
(epoch seconds) and `datasetId`. A response that is not 2xx raises. `save=` writes the bytes
|
|
159
|
+
to that path and the entry to `<path>.pin.json`. The SHA-256 is the one digest the package
|
|
160
|
+
computes: a signed assertion in `queries[]` that no check recomputes.
|
|
161
|
+
- **`badge_cell(bundle_url, *, capture_method, notebook=None, host=None, marimo=False)`** writes
|
|
162
|
+
the verifier badge, linked to `https://typedstandards.org/verify?url=<the bundle URL,
|
|
163
|
+
percent-encoded>` as `@typedstandards/host-core` writes it, above a two-row table (the host and
|
|
164
|
+
the capture method). With `notebook`, it is inserted as the notebook's first cell (id
|
|
165
|
+
`typedstandards-badge` on nbformat 4.5). With `marimo=True`, it returns the source of a
|
|
166
|
+
`mo.md(...)` cell to paste into the app. The cell is written before signing and is part of the
|
|
167
|
+
signed bytes, so it names no hash and no time; a URL or value holding a 64-hex string, a date or
|
|
168
|
+
a time is refused. The URL's host and port are not read as a time (`192.168.1.10:8080` is
|
|
169
|
+
accepted).
|
|
170
|
+
- **`comparison_cell(notebook, values, *, recompute, captured_at)`** appends the comparison cell
|
|
171
|
+
of spec §8.7.4 as the last cell (id `typedstandards-comparison`): the values as Python literals,
|
|
172
|
+
`current = <recompute>`, and a loop that prints each delta. Values are `None`, `bool`, `int`,
|
|
173
|
+
finite `float`, `str`, and lists and str-keyed dicts of those; anything else is refused.
|
|
174
|
+
- **`sidecar(view, artifact, *, directory=None)`** writes the commitment view as YAML (spec
|
|
175
|
+
§8.8.3) beside the artifact: every field `view` printed except the inline `package` and a
|
|
176
|
+
served bundle's `trustRegistry`, which are not §8.8.1 fields. The file is named
|
|
177
|
+
`<artifact's file name>.record.yaml`, extension kept (`analysis.ipynb.record.yaml`): the spec's
|
|
178
|
+
`<artifact-basename>` does not say whether the extension stays, and keeping it is the POSIX
|
|
179
|
+
basename and cannot collide when two artifacts share a stem (`analysis.ipynb` beside
|
|
180
|
+
`analysis.py`).
|
|
181
|
+
- **`show(record, result=None, *, role_path=("role",), marimo=False)`** renders a record (what
|
|
182
|
+
`view` or `sign` printed) with its `verify --json` result: the type, the role, the signer,
|
|
183
|
+
the hash, `createdAt`, the `vcsRef` (marked as asserted and not fetched), the status with its
|
|
184
|
+
reason or successor, one line per check, and a sentence saying that verification does not say
|
|
185
|
+
the analysis is correct. Without `result` it runs `verify`. In Jupyter it returns an object
|
|
186
|
+
with `_repr_html_`; with `marimo=True`, `mo.Html`. The role is a signed assertion the signer
|
|
187
|
+
made, read from the package's `extensions` at `role_path` (by default `extensions["role"]`, a
|
|
188
|
+
string or a list of strings); `show` labels it as the signer's and checks nothing about it.
|
|
189
|
+
|
|
190
|
+
The notebook helpers edit the notebook as JSON, splicing the new cell into `cells` so every
|
|
191
|
+
other byte of the file stays as written. `httpx` is imported only inside `pin`, PyYAML only
|
|
192
|
+
inside `sidecar`, and `marimo` only inside a Marimo call; `IPython`, `marimo` and `nbformat`
|
|
193
|
+
are not dependencies.
|
|
194
|
+
|
|
195
|
+
## Versions
|
|
196
|
+
|
|
197
|
+
Each wrapper release pins one CLI version exactly. A CLI upgrade reaches users as a wrapper release
|
|
198
|
+
that moves the pin.
|
|
199
|
+
|
|
200
|
+
## Development
|
|
201
|
+
|
|
202
|
+
See [CLAUDE.md](https://github.com/npstorey/typedstandards-python/blob/main/CLAUDE.md) for the development loop and the checks CI runs.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT. The wheel also carries each vendored npm package's own licence file: five are MIT, and
|
|
207
|
+
`canonicalize` is Apache-2.0.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Vendor @typedstandards/cli into the wheel (G0 D1 = A).
|
|
2
|
+
|
|
3
|
+
At every wheel build, standard or editable, this hook copies ``package.json`` and
|
|
4
|
+
``package-lock.json`` into ``src/typedstandards/_vendor/`` and runs
|
|
5
|
+
``npm ci --omit=dev --ignore-scripts`` there. The resulting ``node_modules`` tree, with
|
|
6
|
+
each package's own licence file, ships inside the wheel, so an installed wheel needs Node
|
|
7
|
+
and nothing from npm. An editable install (``uv sync``) runs the same hook, so tests drive
|
|
8
|
+
the same tree a user gets.
|
|
9
|
+
|
|
10
|
+
Building needs ``npm`` on ``PATH`` and the npm registry; installing the wheel needs neither.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
22
|
+
|
|
23
|
+
VENDOR = Path("src") / "typedstandards" / "_vendor"
|
|
24
|
+
LICENCE_NAMES = ("LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md", "LICENCE.txt")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VendorCliHook(BuildHookInterface):
|
|
28
|
+
PLUGIN_NAME = "custom"
|
|
29
|
+
|
|
30
|
+
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
|
|
31
|
+
if self.target_name != "wheel":
|
|
32
|
+
return
|
|
33
|
+
root = Path(self.root)
|
|
34
|
+
vendor = root / VENDOR
|
|
35
|
+
npm = shutil.which("npm")
|
|
36
|
+
if npm is None:
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
"building typedstandards needs npm on PATH: the build vendors @typedstandards/cli "
|
|
39
|
+
"with `npm ci --omit=dev --ignore-scripts` (installing the built wheel does not)"
|
|
40
|
+
)
|
|
41
|
+
if vendor.exists():
|
|
42
|
+
shutil.rmtree(vendor)
|
|
43
|
+
vendor.mkdir(parents=True)
|
|
44
|
+
for name in ("package.json", "package-lock.json"):
|
|
45
|
+
shutil.copyfile(root / name, vendor / name)
|
|
46
|
+
self.app.display_info(f"vendoring @typedstandards/cli: npm ci --omit=dev --ignore-scripts in {VENDOR}")
|
|
47
|
+
subprocess.run(
|
|
48
|
+
[npm, "ci", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"],
|
|
49
|
+
cwd=vendor,
|
|
50
|
+
check=True,
|
|
51
|
+
)
|
|
52
|
+
modules = vendor / "node_modules"
|
|
53
|
+
# npm's .bin holds symlinks, which a wheel cannot carry; the wrapper runs the CLI's
|
|
54
|
+
# entry file with node, never through .bin.
|
|
55
|
+
shutil.rmtree(modules / ".bin", ignore_errors=True)
|
|
56
|
+
lock = json.loads((vendor / "package-lock.json").read_text(encoding="utf-8"))
|
|
57
|
+
for key in lock["packages"]:
|
|
58
|
+
if not key:
|
|
59
|
+
continue
|
|
60
|
+
package_dir = vendor / key
|
|
61
|
+
if not any((package_dir / name).is_file() for name in LICENCE_NAMES):
|
|
62
|
+
raise RuntimeError(f"{key} ships no licence file; the wheel must carry each vendored package's licence")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "typedstandards-python-vendored-cli",
|
|
3
|
+
"lockfileVersion": 3,
|
|
4
|
+
"requires": true,
|
|
5
|
+
"packages": {
|
|
6
|
+
"": {
|
|
7
|
+
"name": "typedstandards-python-vendored-cli",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@typedstandards/cli": "0.2.0"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"node_modules/@noble/curves": {
|
|
14
|
+
"version": "2.4.0",
|
|
15
|
+
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.4.0.tgz",
|
|
16
|
+
"integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@noble/hashes": "2.4.0"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">= 20.19.0"
|
|
23
|
+
},
|
|
24
|
+
"funding": {
|
|
25
|
+
"url": "https://paulmillr.com/funding/"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"node_modules/@noble/hashes": {
|
|
29
|
+
"version": "2.4.0",
|
|
30
|
+
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
|
|
31
|
+
"integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">= 20.19.0"
|
|
35
|
+
},
|
|
36
|
+
"funding": {
|
|
37
|
+
"url": "https://paulmillr.com/funding/"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"node_modules/@typedstandards/cli": {
|
|
41
|
+
"version": "0.2.0",
|
|
42
|
+
"resolved": "https://registry.npmjs.org/@typedstandards/cli/-/cli-0.2.0.tgz",
|
|
43
|
+
"integrity": "sha512-hCoB8f8wmKDPZ3LtYRvahohmSvtAlFt6F5y4rI4aH9aDrdpuLeUBzcSmgx7A+9N5+i5T5POInty+O+uCTOalfg==",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@typedstandards/produce-core": "^0.8.0",
|
|
47
|
+
"@typedstandards/verify-core": "^0.13.0"
|
|
48
|
+
},
|
|
49
|
+
"bin": {
|
|
50
|
+
"typedstandards": "dist/bin/main.js"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20.19"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"node_modules/@typedstandards/produce-core": {
|
|
57
|
+
"version": "0.8.0",
|
|
58
|
+
"resolved": "https://registry.npmjs.org/@typedstandards/produce-core/-/produce-core-0.8.0.tgz",
|
|
59
|
+
"integrity": "sha512-5d5xYi3XdfHEFERJ4K7/gtPUwpxhIzzEQqZI9/dYrlBZJQqFgarARVRXUbbHXaPO+1PQ3gFY/1nHjBEw6sD5Gg==",
|
|
60
|
+
"license": "MIT",
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@noble/curves": "^2.2.0",
|
|
63
|
+
"@typedstandards/verify-core": "^0.13.0"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=18"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"node_modules/@typedstandards/verify-core": {
|
|
70
|
+
"version": "0.13.0",
|
|
71
|
+
"resolved": "https://registry.npmjs.org/@typedstandards/verify-core/-/verify-core-0.13.0.tgz",
|
|
72
|
+
"integrity": "sha512-//bMjEiJH0+zSJg6LT3nEjQnFelbpOHPuD6T1ITygLeuS6QMFU1DChB+hFW8QLzxL02UhiUFwoCiLoOVcE2Aew==",
|
|
73
|
+
"license": "MIT",
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"@noble/curves": "^2.2.0",
|
|
76
|
+
"@noble/hashes": "^2.2.0",
|
|
77
|
+
"canonicalize": "^3.0.0"
|
|
78
|
+
},
|
|
79
|
+
"engines": {
|
|
80
|
+
"node": ">=18"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"node_modules/canonicalize": {
|
|
84
|
+
"version": "3.0.0",
|
|
85
|
+
"resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-3.0.0.tgz",
|
|
86
|
+
"integrity": "sha512-yYLfHyDMIXRyRqsKBRLX023riFLpXY2YOfdtqKXZRZy9qsfOJ9U+4F9YZL7MEzL5+ziN2x2nlBvY/Voi3EBljA==",
|
|
87
|
+
"license": "Apache-2.0",
|
|
88
|
+
"bin": {
|
|
89
|
+
"canonicalize": "bin/canonicalize.js"
|
|
90
|
+
},
|
|
91
|
+
"engines": {
|
|
92
|
+
"node": ">=18"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "typedstandards-python-vendored-cli",
|
|
3
|
+
"private": true,
|
|
4
|
+
"description": "The CLI the typedstandards wheel vendors. The hatchling build hook (hatch_build.py) runs npm ci --omit=dev --ignore-scripts against this file and package-lock.json, and ships the tree inside the wheel.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@typedstandards/cli": "0.2.0"
|
|
8
|
+
}
|
|
9
|
+
}
|