csl-pyutil 0.24.1__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.
- csl_pyutil-0.24.1/LICENSE +21 -0
- csl_pyutil-0.24.1/PKG-INFO +15 -0
- csl_pyutil-0.24.1/README.md +255 -0
- csl_pyutil-0.24.1/csl_pyutil/__init__.py +68 -0
- csl_pyutil-0.24.1/csl_pyutil/anatomy.py +237 -0
- csl_pyutil-0.24.1/csl_pyutil/evidence.py +512 -0
- csl_pyutil-0.24.1/csl_pyutil/integrity_tripwire.py +679 -0
- csl_pyutil-0.24.1/csl_pyutil/review_sheet.py +3458 -0
- csl_pyutil-0.24.1/csl_pyutil.egg-info/PKG-INFO +15 -0
- csl_pyutil-0.24.1/csl_pyutil.egg-info/SOURCES.txt +31 -0
- csl_pyutil-0.24.1/csl_pyutil.egg-info/dependency_links.txt +1 -0
- csl_pyutil-0.24.1/csl_pyutil.egg-info/requires.txt +3 -0
- csl_pyutil-0.24.1/csl_pyutil.egg-info/top_level.txt +1 -0
- csl_pyutil-0.24.1/pyproject.toml +28 -0
- csl_pyutil-0.24.1/setup.cfg +4 -0
- csl_pyutil-0.24.1/tests/test_anatomy.py +100 -0
- csl_pyutil-0.24.1/tests/test_evidence.py +286 -0
- csl_pyutil-0.24.1/tests/test_export_context.py +60 -0
- csl_pyutil-0.24.1/tests/test_fixture_byte_identical.py +30 -0
- csl_pyutil-0.24.1/tests/test_generator_meta.py +24 -0
- csl_pyutil-0.24.1/tests/test_handin.py +127 -0
- csl_pyutil-0.24.1/tests/test_identity_gate.py +81 -0
- csl_pyutil-0.24.1/tests/test_integrity_tripwire.py +647 -0
- csl_pyutil-0.24.1/tests/test_mobile_css.py +16 -0
- csl_pyutil-0.24.1/tests/test_review_sheet.py +639 -0
- csl_pyutil-0.24.1/tests/test_ru_ui_strings.py +42 -0
- csl_pyutil-0.24.1/tests/test_split_layout.py +98 -0
- csl_pyutil-0.24.1/tests/test_timing.py +64 -0
- csl_pyutil-0.24.1/tests/test_typology_stats.py +74 -0
- csl_pyutil-0.24.1/tests/test_v15_session_flow.py +405 -0
- csl_pyutil-0.24.1/tests/test_v16_packset.py +463 -0
- csl_pyutil-0.24.1/tests/test_v17_vote_ux.py +204 -0
- csl_pyutil-0.24.1/tests/test_version_strings.py +35 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sanskrit Lexicon / Cologne Digital Sanskrit Dictionaries contributors
|
|
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,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csl-pyutil
|
|
3
|
+
Version: 0.24.1
|
|
4
|
+
Summary: Generic (non-Sanskrit-specific) Python helpers shared across the CDSL / Sanskrit-Lexicon repos
|
|
5
|
+
Author: Sanskrit Lexicon / CDSL
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sanskrit-lexicon/csl-pyutil
|
|
8
|
+
Keywords: cdsl,lexicography,review-sheet,tooling
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest; extra == "test"
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# csl-pyutil
|
|
2
|
+
|
|
3
|
+
_Created: 14-07-2026 · Last updated: 05-09-2026_
|
|
4
|
+
|
|
5
|
+
Generic (non-Sanskrit-specific) Python helpers shared across the CDSL /
|
|
6
|
+
Sanskrit-Lexicon repos. Distinct from
|
|
7
|
+
[`sanskrit-util`](https://github.com/sanskrit-lexicon/sanskrit-util), which is
|
|
8
|
+
scoped narrowly to Sanskrit string/transcoding helpers (IAST/SLP1/Devanāgarī)
|
|
9
|
+
— this package is for everything else that shouldn't be re-typed per repo but
|
|
10
|
+
also isn't Sanskrit-linguistics-specific.
|
|
11
|
+
|
|
12
|
+
## Why this exists
|
|
13
|
+
|
|
14
|
+
A [cross-repo dev-status + reuse review](https://github.com/gasyoun/Uprava/blob/main/CROSS_REPO_DEV_STATUS_AND_REUSE_REVIEW_07.2026.md)
|
|
15
|
+
found **six independently hand-rolled ~150–250-line HTML review-sheet shells**
|
|
16
|
+
across four repos — all implementing the same approve/reject/defer pattern
|
|
17
|
+
(running tally, `localStorage` persistence, a/r/d keyboard shortcuts, free-text
|
|
18
|
+
notes, `Download decisions.json` with an identical
|
|
19
|
+
`{sheet_id, generated, decided, items}` schema) — with **no shared import
|
|
20
|
+
between any of them**. Root cause: the org's `/review-sheet` Claude Code skill
|
|
21
|
+
is prose-only, so every invocation re-derived the same markup/JS from
|
|
22
|
+
scratch. Proven drift: none of the six shells implemented the skill-mandated
|
|
23
|
+
`showSaveFilePicker` auto-save + legend footer (H779, 12-07-2026) — manual
|
|
24
|
+
propagation had already failed once.
|
|
25
|
+
|
|
26
|
+
## `render_review_sheet()`
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pip install "csl-pyutil @ git+https://github.com/sanskrit-lexicon/csl-pyutil@main"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from csl_pyutil import render_review_sheet
|
|
34
|
+
|
|
35
|
+
html = render_review_sheet(
|
|
36
|
+
items=[
|
|
37
|
+
{"id": "L142", "filt": "typeA", "title": "headword or item label",
|
|
38
|
+
"badges": ["tag1", "tag2"],
|
|
39
|
+
"question": "the judgment question shown on the card (HTML allowed)",
|
|
40
|
+
"panels": [("Panel heading", "<pre>panel body, HTML allowed</pre>")],
|
|
41
|
+
"note_placeholder": "optional custom placeholder text"},
|
|
42
|
+
...
|
|
43
|
+
],
|
|
44
|
+
config={
|
|
45
|
+
"sheet_id": "commentarystrategies-sundarakanda_35-37", # org naming convention
|
|
46
|
+
"title": "Sundarakāṇḍa commentary xref — sarga 35-37",
|
|
47
|
+
"subtitle": "43 candidate cross-references needing a decision",
|
|
48
|
+
"footer": "Approve = accept the xref. Reject = discard. Defer = unsure.",
|
|
49
|
+
"approve_label": "Approve", "reject_label": "Reject",
|
|
50
|
+
"filters": [("typeA", "Type A"), ("typeB", "Type B")],
|
|
51
|
+
"generated": "2026-07-14", # caller-supplied, never computed here — deterministic output
|
|
52
|
+
# Optional: make final exports publication-review admissible.
|
|
53
|
+
"strict_review": {
|
|
54
|
+
"reviewer": "gasyoun",
|
|
55
|
+
"require_all_votes": True,
|
|
56
|
+
"require_reject_note": True,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
# Required when extras=True (default) since v0.8.0 / H1649:
|
|
60
|
+
screening={
|
|
61
|
+
"deterministic": 0,
|
|
62
|
+
"lookup": 0,
|
|
63
|
+
"agent": 0,
|
|
64
|
+
"human": 43,
|
|
65
|
+
"evidence_path": "review/screening_evidence.md",
|
|
66
|
+
"rules": ["none"], # or the real rule names applied
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
open("review/commentarystrategies-sundarakanda_35-37_review.html", "w",
|
|
70
|
+
encoding="utf-8").write(html)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Ported byte-for-byte (proven by a fixture test, `tests/test_fixture_byte_identical.py`)
|
|
74
|
+
from
|
|
75
|
+
[`SanskritLexicography/RussianTranslation/src/build_h180_review_sheets.py`](https://github.com/gasyoun/SanskritLexicography/blob/master/RussianTranslation/src/build_h180_review_sheets.py)
|
|
76
|
+
— the richest of the six hand-rolled shells (H925). `extras=True` (the
|
|
77
|
+
default) additionally folds in what H779 mandated but no shell implemented: a
|
|
78
|
+
File System Access API "Save to folder" auto-save control and a button-legend
|
|
79
|
+
footer. Pass `extras=False` only to reproduce a pre-H779 shell's literal
|
|
80
|
+
historical output.
|
|
81
|
+
|
|
82
|
+
Exported decisions-JSON shape (unchanged from every shell's prior contract, so
|
|
83
|
+
[`Uprava/tools/review_decisions_watcher.py`](https://github.com/gasyoun/Uprava/blob/main/tools/review_decisions_watcher.py)
|
|
84
|
+
needs no changes):
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{"sheet_id": "...", "generated": "2026-07-14T12:00:00.000Z", "decided": 12,
|
|
88
|
+
"items": [{"id": "L142", "decision": "approve", "note": ""}, ...]}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
With `strict_review`, the same payload gains additive top-level fields:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{"sheet_id": "...", "generated": "2026-07-14", "decided": 12,
|
|
95
|
+
"reviewer": "gasyoun", "reviewedAt": "2026-07-17T20:45:00.000Z",
|
|
96
|
+
"complete": true,
|
|
97
|
+
"items": [{"id": "L142", "decision": "approve", "note": ""}, ...]}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Partial File System Access API auto-saves use `complete: false` and
|
|
101
|
+
`reviewedAt: null`. Final download is blocked until the reviewer is non-empty,
|
|
102
|
+
every item is voted, and every rejected item has a note. Callers that omit
|
|
103
|
+
`strict_review` retain the 0.1.x behavior and byte-identical core rendering.
|
|
104
|
+
|
|
105
|
+
See [`csl_pyutil/review_sheet.py`](https://github.com/sanskrit-lexicon/csl-pyutil/blob/main/csl_pyutil/review_sheet.py) for the full
|
|
106
|
+
item/config schema docstring.
|
|
107
|
+
|
|
108
|
+
### Presentation (0.5.0)
|
|
109
|
+
|
|
110
|
+
`config["font_scale"]` multiplies the whole type scale and **defaults to 1.5** —
|
|
111
|
+
MG's "+150%", ruled 28-07-2026 while voting the G5 sheet, together with a fix to
|
|
112
|
+
the inherited hierarchy: the panel `<pre>` holding the text under judgement used
|
|
113
|
+
to be the smallest type on the page. An A−/A+ toolbar control re-points the scale
|
|
114
|
+
per browser. `font_scale=1` restores the donor sizes; `extras=False` never gets
|
|
115
|
+
the layer, so the byte-identical fixture stands.
|
|
116
|
+
|
|
117
|
+
`config["extra_css"]` appends caller CSS last in the cascade.
|
|
118
|
+
|
|
119
|
+
### The evidence gate — V9 / V10 (0.9.0)
|
|
120
|
+
|
|
121
|
+
V1–V8 are entirely about *presentation*. A sheet can be green on every one of them
|
|
122
|
+
and still ask a human to re-derive, by eye, a conclusion the repo already holds on
|
|
123
|
+
disk — measured 29-07-2026 on the sheet that prompted this: **191 of 200 cards**
|
|
124
|
+
already carried a machine verdict, a named rule and cited evidence computed from
|
|
125
|
+
the same inputs, none of it rendered, and **69 of 200** were not disagreements at
|
|
126
|
+
all. So the emitter now refuses to write those sheets.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from csl_pyutil import EvidenceManifest, render_review_sheet
|
|
130
|
+
|
|
131
|
+
man = EvidenceManifest(sheet_id=SHEET_ID, row_ids=[i["id"] for i in items],
|
|
132
|
+
repo_root=REPO)
|
|
133
|
+
man.declare_joined("research/adjudication.tsv", ["verdict", "rule", "reason"])
|
|
134
|
+
man.declare_omitted_path("research/superseded_run.tsv",
|
|
135
|
+
because="superseded by the H1681 rerun, kept for audit")
|
|
136
|
+
man.declare_omitted("DCS attested sentence",
|
|
137
|
+
because="no per-compound sentence map exists; only sense-level")
|
|
138
|
+
for it in items:
|
|
139
|
+
man.add_card(it["id"], evidence_fields=["verdict", "rule"])
|
|
140
|
+
|
|
141
|
+
html = render_review_sheet(items, config, screening=..., manifest=man) # raises, or returns
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**V9 — evidence reuse.** With `manifest=`, [`preflight()`](https://github.com/sanskrit-lexicon/csl-pyutil/blob/main/csl_pyutil/evidence.py)
|
|
145
|
+
runs against the *finished* document (after `ui_strings`, so it sees exactly what the
|
|
146
|
+
reviewer sees) and raises `PreflightError` before a byte is returned: undeclared
|
|
147
|
+
prior art keyed on the same row ids, cards under the evidence floor with no stated
|
|
148
|
+
reason, Cyrillic/IAST mixed inside one word, SLP1 leaking into human-facing text,
|
|
149
|
+
and structurally impossible sūtra citations. Tune it with `config["preflight"]`
|
|
150
|
+
(`allow_slp1_tokens`, `overlap_threshold`, `skip_prior_art`). Without `manifest=`
|
|
151
|
+
you get a `PreflightWarning` naming the reason — a migration ramp for the pre-0.9.0
|
|
152
|
+
generators that **becomes an error in 1.0.0**; escalate it today with
|
|
153
|
+
`-W error::csl_pyutil.evidence.PreflightWarning`.
|
|
154
|
+
|
|
155
|
+
**V10 — no non-decisions.** `config["non_decision_share"]` is the largest fraction
|
|
156
|
+
of cards the sheet may carry that your own pre-filter already resolved, marked per
|
|
157
|
+
card as `item["machine_resolvable"] = True`. The caller classifies (only it knows its
|
|
158
|
+
domain); the emitter enforces. It **defaults to 0.0** — a card the machine has
|
|
159
|
+
answered does not belong on a human's plate — and a sheet with no flagged item is
|
|
160
|
+
unaffected.
|
|
161
|
+
|
|
162
|
+
Two asymmetries in the gate are deliberate and load-bearing: a conceptual
|
|
163
|
+
`declare_omitted()` can never silence a real file the scan found (only
|
|
164
|
+
`declare_omitted_path()` can), and the SLP1 detector stays **silent** on the
|
|
165
|
+
undecidable all-lowercase case (`agni + deva` is byte-identical in SLP1 and IAST)
|
|
166
|
+
rather than guessing.
|
|
167
|
+
|
|
168
|
+
## `anatomy` — colour-coded CDSL raw markup
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from csl_pyutil import anatomy
|
|
172
|
+
|
|
173
|
+
panel_body = anatomy.highlight(raw_record, target="As") # tinted, markup still visible
|
|
174
|
+
legend = anatomy.legend_html(parts=["sanskrit", "gloss", "citation"])
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A raw CDSL record dumped verbatim into a review card is a wall of punctuation.
|
|
178
|
+
`highlight()` keeps every tag **visible** — the tags are the anatomy — but dims the
|
|
179
|
+
delimiters and colours each payload by part class (Sanskrit form · gloss · citation ·
|
|
180
|
+
grammar · abbreviation · cross-reference · etymology · homonym), outlining any form
|
|
181
|
+
equal to `target`. Written for csl-atlas's xref sheet (H1646), moved here under H1808
|
|
182
|
+
when a second generator needed it; csl-atlas's `scripts/lib/cdsl_anatomy.py` is now a
|
|
183
|
+
re-export shim so there is one canonical copy.
|
|
184
|
+
|
|
185
|
+
Hooks for what a shared module cannot know:
|
|
186
|
+
|
|
187
|
+
| Argument | Use |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `tag_parts={"ab": "crossref"}` | override the tag → part map (PWG wraps *every* abbreviation in `<ab>`; an xref sheet wants the brighter class) |
|
|
190
|
+
| `plain_hook(text)` | reach text the markup does not delimit — NWS-layer cards carry citations as bare text with no `<ls>` around them |
|
|
191
|
+
| `payload_hook(part, inner, attrs)` | render a tagged payload yourself, e.g. `<ls>` as a Cologne source link |
|
|
192
|
+
|
|
193
|
+
The sheet's naming, placement (gitignored `review/`), GTD `@DO` line, and
|
|
194
|
+
`Uprava/REVIEW_SHEETS_INDEX.md` registration are still the caller's job — this
|
|
195
|
+
function only produces the HTML string. See
|
|
196
|
+
[`~/.claude/commands/review-sheet.md`](https://github.com/gasyoun/claude-config/blob/main/commands/review-sheet.md)
|
|
197
|
+
for the full process.
|
|
198
|
+
|
|
199
|
+
## `integrity_tripwire` — a committed checksum on human-reviewed data
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
python -m csl_pyutil.integrity_tripwire --check --pin data/integrity/<store>.pin.json
|
|
203
|
+
python -m csl_pyutil.integrity_tripwire --extract --pin data/integrity/<store>.pin.json \
|
|
204
|
+
--write-pin --reason "what changed and why"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Three repos have each lost human review work to an automated writer that left a
|
|
208
|
+
file that still parsed: csl-atlas's `--reseed` drops every preserved ruling,
|
|
209
|
+
eleven WhitneyRoots `apply_*` scripts open `src/app_data.json` with `'w'` and no
|
|
210
|
+
lock, and pwg_ru's own review-applying script is unlocked and un-gated. Nobody
|
|
211
|
+
noticed for months, because nothing was watching. This module is what watches.
|
|
212
|
+
|
|
213
|
+
It hashes a **projection** — only the key fields and the reviewed fields of the
|
|
214
|
+
rows a store's predicate calls reviewed — so a reformat, a reorder, or an edit
|
|
215
|
+
to unreviewed content is invisible, and a dropped `reviewer` stamp is not. A
|
|
216
|
+
whole-file SHA-256 was tried first and cried wolf (a 1.29 MB serializer shrink
|
|
217
|
+
at an identical row count), which is how it came to be ignored. Two digests:
|
|
218
|
+
`overlay_sha256` for *did the reviewed content change*, `keyset_sha256` for
|
|
219
|
+
*did the reviewed row set change* — an in-place overwrite moves only the first,
|
|
220
|
+
a deleted row only the second.
|
|
221
|
+
|
|
222
|
+
The rule for "a row is reviewed" is **data, not code**: it lives in the `spec`
|
|
223
|
+
block of each pin, copied from the org census, because the three stores share no
|
|
224
|
+
review stamp. pwg_ru has `reviewer`/`review_status`/`editorial_decision*`;
|
|
225
|
+
csl-atlas needs `reviewStatus` **and** a human `reviewer` (137 of its 147
|
|
226
|
+
status-reviewed rows are agent-attributed, so status alone over-claims human
|
|
227
|
+
review 14.7×); WhitneyRoots has no per-row stamp at all and is reviewed
|
|
228
|
+
file-level.
|
|
229
|
+
|
|
230
|
+
CI runs `--check` and **fails** on a mismatch. A legitimate change regenerates
|
|
231
|
+
the pin in the same commit with a one-line `reason` — that is the whole
|
|
232
|
+
acknowledgement ritual. Exit codes: `0` match, `1` tripwire, `2` broken spec
|
|
233
|
+
(a defect must not be able to look like a clean store).
|
|
234
|
+
|
|
235
|
+
Gitignored stores (pwg_ru's live 26 MB JSONL) commit a derived `--extract`
|
|
236
|
+
projection instead, so CI can check what it cannot see. The live bytes never
|
|
237
|
+
enter git: `redact_fields` names reviewed fields whose value is replaced by
|
|
238
|
+
`sha256:<hex>` of itself, so a field like pwg_ru's `human_review` — the
|
|
239
|
+
curator's verbatim free-text notes, watched precisely because a wipe of it is
|
|
240
|
+
the failure mode — stays fully covered by the digest while publishing nothing
|
|
241
|
+
into a public repo. The hash moves the instant a single character does.
|
|
242
|
+
|
|
243
|
+
## Tests
|
|
244
|
+
|
|
245
|
+
```sh
|
|
246
|
+
pip install -e . pytest
|
|
247
|
+
pytest tests -q
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`tests/fixtures/` holds 100%-synthetic placeholder content generated only
|
|
251
|
+
through the donor's own render functions — never real translation data (the
|
|
252
|
+
donor's own gitignored/unpublished pwg_ru store must never land in this
|
|
253
|
+
public repo).
|
|
254
|
+
|
|
255
|
+
_Dr. Mārcis Gasūns_
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""csl_pyutil — generic (non-Sanskrit-specific) Python helpers shared across
|
|
3
|
+
the CDSL / Sanskrit-Lexicon repos.
|
|
4
|
+
|
|
5
|
+
Public API
|
|
6
|
+
----------
|
|
7
|
+
render_review_sheet(items, config, extras=True) self-contained HTML review/
|
|
8
|
+
voting sheet (H925)
|
|
9
|
+
render_review_sheet_packset(items, config, ...) the same sheet split into
|
|
10
|
+
packs of 10 sharing one
|
|
11
|
+
sheet_id, plus an index
|
|
12
|
+
page (V16, H2991)
|
|
13
|
+
anatomy.highlight(raw, target=None, ...) colour-coded CDSL raw-markup
|
|
14
|
+
anatomy.legend_html(parts=None, ...) anatomy for a panel (H1808)
|
|
15
|
+
evidence.EvidenceManifest / evidence.preflight the V9 evidence-reuse gate a
|
|
16
|
+
sheet must pass before it is
|
|
17
|
+
written (H1889)
|
|
18
|
+
RU_UI_STRINGS one-line Russian chrome
|
|
19
|
+
preset for config["ui_strings"]
|
|
20
|
+
(H2854)
|
|
21
|
+
integrity_tripwire.check / .extract committed checksum + key-set
|
|
22
|
+
on human-reviewed overlay
|
|
23
|
+
data, red in CI when a
|
|
24
|
+
seeder wipes it (H2891)
|
|
25
|
+
"""
|
|
26
|
+
from csl_pyutil import anatomy, evidence
|
|
27
|
+
from csl_pyutil.evidence import EvidenceManifest, PreflightError, PreflightWarning, preflight
|
|
28
|
+
from csl_pyutil.review_sheet import (render_review_sheet, render_review_sheet_packset,
|
|
29
|
+
esc, mark_cyrillic, RU_UI_STRINGS)
|
|
30
|
+
|
|
31
|
+
# integrity_tripwire is imported LAZILY (PEP 562), not eagerly like its
|
|
32
|
+
# neighbours. Its documented CI invocation is `python -m
|
|
33
|
+
# csl_pyutil.integrity_tripwire --check`, and runpy warns "found in sys.modules
|
|
34
|
+
# after import of package" whenever the package has already pulled the module
|
|
35
|
+
# in — which it would, on every single tripwire run in every consumer repo. A
|
|
36
|
+
# gate whose job is to be believed when it prints RED must not also print a
|
|
37
|
+
# spurious RuntimeWarning every time it prints GREEN.
|
|
38
|
+
_LAZY = {
|
|
39
|
+
"integrity_tripwire": None,
|
|
40
|
+
"TripwireError": "integrity_tripwire",
|
|
41
|
+
"project": "integrity_tripwire",
|
|
42
|
+
"overlay_digest": "integrity_tripwire",
|
|
43
|
+
"keyset_digest": "integrity_tripwire",
|
|
44
|
+
"is_reviewed": "integrity_tripwire",
|
|
45
|
+
"extract": "integrity_tripwire",
|
|
46
|
+
"check": "integrity_tripwire",
|
|
47
|
+
"redact": "integrity_tripwire",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def __getattr__(name):
|
|
52
|
+
if name in _LAZY:
|
|
53
|
+
import importlib
|
|
54
|
+
|
|
55
|
+
module = importlib.import_module("csl_pyutil.integrity_tripwire")
|
|
56
|
+
return module if _LAZY[name] is None else getattr(module, name)
|
|
57
|
+
raise AttributeError("module %r has no attribute %r" % (__name__, name))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __dir__():
|
|
61
|
+
return sorted(list(globals()) + list(_LAZY))
|
|
62
|
+
|
|
63
|
+
__version__ = "0.24.1"
|
|
64
|
+
__all__ = ["render_review_sheet", "render_review_sheet_packset", "esc", "mark_cyrillic",
|
|
65
|
+
"RU_UI_STRINGS", "anatomy", "evidence",
|
|
66
|
+
"EvidenceManifest", "PreflightError", "PreflightWarning", "preflight",
|
|
67
|
+
"integrity_tripwire", "TripwireError", "project", "overlay_digest",
|
|
68
|
+
"keyset_digest", "is_reviewed", "extract", "check"]
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""anatomy — colour-code the anatomy of a raw CDSL dictionary record.
|
|
3
|
+
|
|
4
|
+
A CDSL record body is a dense mix of SGML-ish tags (``<s>``, ``<lex>``, ``<ls>``,
|
|
5
|
+
``<ab>``) and brace markers (``{#…#}``, ``{%…%}``) whose classes differ per
|
|
6
|
+
dictionary. Dumped verbatim into a review card it is a wall of punctuation, and a
|
|
7
|
+
reviewer cannot see which clause the judgement rests on (H1646: "add dictionary
|
|
8
|
+
entry anatomy markup, the bright colors for different part of entry").
|
|
9
|
+
|
|
10
|
+
This module keeps the markup fully VISIBLE — the tags are the anatomy, not noise
|
|
11
|
+
to be stripped — but dims the delimiters and colours the payload by part class, so
|
|
12
|
+
the shape of the entry reads at a glance.
|
|
13
|
+
|
|
14
|
+
Provenance: written for csl-atlas's xref sheet (H1646) as
|
|
15
|
+
``scripts/lib/cdsl_anatomy.py``, lifted here unchanged in behaviour under H1808
|
|
16
|
+
when a SECOND sheet generator (SanskritLexicography's G5 print-readiness lane)
|
|
17
|
+
turned out to need the same thing and had no way to reach it — MG, voting that
|
|
18
|
+
sheet: "why entry anatomy is missing again? It must be a hook". One canonical
|
|
19
|
+
copy in the shared emitter's package is that fix; csl-atlas's file is now a
|
|
20
|
+
re-export shim.
|
|
21
|
+
|
|
22
|
+
Prior art it deliberately reuses rather than re-derives:
|
|
23
|
+
|
|
24
|
+
* Part taxonomy and colour semantics — the ``/entry-anatomy`` skill
|
|
25
|
+
(``entry_anatomy.py``'s ``PARTS`` / ``DICT_MAPS``), which segments a CDSL entry
|
|
26
|
+
into headword · grammar · etymology · sense · citation · cross-reference.
|
|
27
|
+
* The raw-markup highlighting approach and dark palette —
|
|
28
|
+
``SanskritLexicography/EntryAnatomy/build_entry_anatomy.py`` ``raw_highlight()``
|
|
29
|
+
+ ``GENERIC_EXTRA_CSS``, whose colours already sit on a dark panel.
|
|
30
|
+
|
|
31
|
+
Colours are INLINE ``style=`` attributes, not a stylesheet: the output has to drop
|
|
32
|
+
into any panel body of any sheet, including callers that pass no ``extra_css``.
|
|
33
|
+
The container carries ``class="anatomy"`` so the emitter's type scale can still
|
|
34
|
+
reach it (the ``!important`` scale layer outranks the inline ``font`` shorthand).
|
|
35
|
+
"""
|
|
36
|
+
import html
|
|
37
|
+
import re
|
|
38
|
+
|
|
39
|
+
#: Part class -> (colour, human label, extra CSS). Dark-panel palette, matching the
|
|
40
|
+
#: review sheet's own --panel2 (#1e222b). Labels drive the rendered legend.
|
|
41
|
+
#: Labels are Russian: the surfaces rendering this legend are review sheets, whose
|
|
42
|
+
#: reviewer reads Russian (H1648). Keys stay English machine identifiers.
|
|
43
|
+
PARTS = {
|
|
44
|
+
"sanskrit": ("#e6c07b", "санскритская форма", ""),
|
|
45
|
+
"gloss": ("#98c379", "перевод / значение", "font-style:italic"),
|
|
46
|
+
"citation": ("#e06c75", "ссылка на источник", ""),
|
|
47
|
+
"grammar": ("#d19a66", "грамматическая помета", ""),
|
|
48
|
+
"abbreviation": ("#c9a227", "сокращение (<ab>)", ""),
|
|
49
|
+
"crossref": ("#56b6c2", "маркер перекрёстной ссылки (cf. / Vgl.)", "font-weight:600"),
|
|
50
|
+
"etymology": ("#61afef", "этимология / когнат", ""),
|
|
51
|
+
"language": ("#7aa2c9", "название языка", ""),
|
|
52
|
+
"taxon": ("#c678dd", "ботаническое / зоологическое название", ""),
|
|
53
|
+
"homonym": ("#b57edc", "номер омонима", ""),
|
|
54
|
+
"structure": ("#7f8c9b", "разделитель значения / раздела", ""),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#: Paired content tags -> part class. ``<s>``/``<s1>``/``<s2>`` are MW's Sanskrit
|
|
58
|
+
#: spans; ``<is>`` is PWG's.
|
|
59
|
+
#:
|
|
60
|
+
#: ``<ab>`` defaults to ``abbreviation`` — in PWG it wraps EVERY abbreviation
|
|
61
|
+
#: (``caus.``, ``gerund.``, ``v. a.``), of which cf./Vgl. is one case. csl-atlas's
|
|
62
|
+
#: xref sheet judges cross-references specifically and wants the brighter
|
|
63
|
+
#: ``crossref`` treatment, so it passes ``tag_parts={"ab": "crossref"}``.
|
|
64
|
+
TAG_PARTS = {
|
|
65
|
+
"s": "sanskrit", "s1": "sanskrit", "s2": "sanskrit", "is": "sanskrit",
|
|
66
|
+
"ns": "gloss",
|
|
67
|
+
"ls": "citation",
|
|
68
|
+
"lex": "grammar",
|
|
69
|
+
"ab": "abbreviation",
|
|
70
|
+
"etym": "etymology",
|
|
71
|
+
"lang": "language",
|
|
72
|
+
"bot": "taxon", "zoo": "taxon",
|
|
73
|
+
"hom": "homonym",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#: Brace markers -> part class. ``{#…#}`` Sanskrit, ``{%…%}`` gloss (PWG/AP90),
|
|
77
|
+
#: ``{@…@}`` a sense/section number.
|
|
78
|
+
BRACE_PARTS = {"#": "sanskrit", "%": "gloss", "@": "structure"}
|
|
79
|
+
|
|
80
|
+
_DELIM = "#5c6773" # tag/brace delimiters — present but receded
|
|
81
|
+
_ACCENT = "#ff7b72" # Vedic accent marks inside a Sanskrit form
|
|
82
|
+
_PIPE = "#e06c75" # the ¦ head/body separator
|
|
83
|
+
_PLAIN = "#d8dce2" # untagged running text
|
|
84
|
+
|
|
85
|
+
_SCANNER = re.compile(
|
|
86
|
+
r"(?P<pair><(?P<tag>s1|s2|s|is|ns|ls|lex|ab|etym|lang|bot|zoo|hom)\b(?P<attrs>[^>]*)>"
|
|
87
|
+
r"(?P<inner>.*?)</(?P=tag)>)"
|
|
88
|
+
r"|(?P<brace>\{(?P<bk>[#%@])(?P<binner>.*?)(?P=bk)\})"
|
|
89
|
+
r"|(?P<other><[^>]+>)"
|
|
90
|
+
r"|(?P<pipe>¦)",
|
|
91
|
+
re.DOTALL,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
#: Vedic accent / length marks CDSL writes inside SLP1 forms. Stripped only when
|
|
95
|
+
#: comparing a form against the highlight target.
|
|
96
|
+
_ACCENT_CHARS = "/\\^~"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _strip_accents(text):
|
|
100
|
+
return "".join(ch for ch in text if ch not in _ACCENT_CHARS)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _span(text, colour, extra="", title=None, escape=True):
|
|
104
|
+
body = html.escape(text) if escape else text
|
|
105
|
+
style = "color:%s" % colour
|
|
106
|
+
if extra:
|
|
107
|
+
style += ";" + extra
|
|
108
|
+
attrs = ' title="%s"' % html.escape(title) if title else ""
|
|
109
|
+
return '<span style="%s"%s>%s</span>' % (style, attrs, body)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _sanskrit_body(inner, target_norm):
|
|
113
|
+
"""Colour a Sanskrit payload, marking accents and the highlight target."""
|
|
114
|
+
colour, _label, extra = PARTS["sanskrit"]
|
|
115
|
+
pieces = []
|
|
116
|
+
for ch in inner:
|
|
117
|
+
if ch in _ACCENT_CHARS:
|
|
118
|
+
pieces.append(_span(ch, _ACCENT, "font-weight:700"))
|
|
119
|
+
else:
|
|
120
|
+
pieces.append(html.escape(ch))
|
|
121
|
+
body = "".join(pieces)
|
|
122
|
+
if target_norm and _strip_accents(inner).strip() == target_norm:
|
|
123
|
+
# This span IS the form the card is asking about.
|
|
124
|
+
return (
|
|
125
|
+
'<span style="background:rgba(86,182,194,.22);outline:1px solid #56b6c2;'
|
|
126
|
+
'border-radius:3px;padding:0 2px" title="цель перекрёстной ссылки, о которой спрашивает эта карточка">'
|
|
127
|
+
+ _span(body, colour, extra, escape=False)
|
|
128
|
+
+ "</span>"
|
|
129
|
+
)
|
|
130
|
+
return _span(body, colour, extra, escape=False)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def highlight(raw, target=None, *, tag_parts=None, plain_hook=None, payload_hook=None):
|
|
134
|
+
"""Return colour-coded HTML for one raw CDSL record body.
|
|
135
|
+
|
|
136
|
+
``target`` is the SLP1 form under judgement; every Sanskrit span in the
|
|
137
|
+
record equal to it (ignoring accent marks) is outlined.
|
|
138
|
+
|
|
139
|
+
``tag_parts`` overrides the tag -> part mapping for this call (merged over
|
|
140
|
+
``TAG_PARTS``), e.g. ``{"ab": "crossref"}``.
|
|
141
|
+
|
|
142
|
+
``plain_hook(text) -> html`` is called for every UNTAGGED run. A caller uses
|
|
143
|
+
it to reach content the markup does not delimit — bare citations, bracketed
|
|
144
|
+
diasystem tags — and is responsible for escaping what it returns. Return
|
|
145
|
+
``None`` to fall through to the default plain rendering.
|
|
146
|
+
|
|
147
|
+
``payload_hook(part, inner, attrs) -> html`` is called for each tagged
|
|
148
|
+
payload before it is coloured; a caller returns ready HTML (e.g. an ``<ls>``
|
|
149
|
+
citation rendered as a source link) or ``None`` to fall through.
|
|
150
|
+
"""
|
|
151
|
+
text = str(raw or "")
|
|
152
|
+
target_norm = _strip_accents(str(target or "").strip()) or None
|
|
153
|
+
parts_map = dict(TAG_PARTS)
|
|
154
|
+
if tag_parts:
|
|
155
|
+
parts_map.update(tag_parts)
|
|
156
|
+
|
|
157
|
+
def plain(chunk):
|
|
158
|
+
if plain_hook is not None:
|
|
159
|
+
got = plain_hook(chunk)
|
|
160
|
+
if got is not None:
|
|
161
|
+
return got
|
|
162
|
+
return _span(chunk, _PLAIN)
|
|
163
|
+
|
|
164
|
+
out = []
|
|
165
|
+
pos = 0
|
|
166
|
+
for m in _SCANNER.finditer(text):
|
|
167
|
+
if m.start() > pos:
|
|
168
|
+
out.append(plain(text[pos:m.start()]))
|
|
169
|
+
if m.group("pair"):
|
|
170
|
+
tag, attrs, inner = m.group("tag"), m.group("attrs") or "", m.group("inner")
|
|
171
|
+
part = parts_map.get(tag, "structure")
|
|
172
|
+
colour, label, extra = PARTS[part]
|
|
173
|
+
out.append(_span("<%s%s>" % (tag, attrs), _DELIM, title=label))
|
|
174
|
+
hooked = payload_hook(part, inner, attrs) if payload_hook is not None else None
|
|
175
|
+
if hooked is not None:
|
|
176
|
+
out.append(hooked)
|
|
177
|
+
elif part == "sanskrit":
|
|
178
|
+
out.append(_sanskrit_body(inner, target_norm))
|
|
179
|
+
else:
|
|
180
|
+
out.append(_span(inner, colour, extra, title=label))
|
|
181
|
+
out.append(_span("</%s>" % tag, _DELIM, title=label))
|
|
182
|
+
elif m.group("brace"):
|
|
183
|
+
bk, inner = m.group("bk"), m.group("binner")
|
|
184
|
+
part = BRACE_PARTS.get(bk, "structure")
|
|
185
|
+
colour, label, extra = PARTS[part]
|
|
186
|
+
out.append(_span("{" + bk, _DELIM, title=label))
|
|
187
|
+
if part == "sanskrit":
|
|
188
|
+
out.append(_sanskrit_body(inner, target_norm))
|
|
189
|
+
else:
|
|
190
|
+
out.append(_span(inner, colour, extra, title=label))
|
|
191
|
+
out.append(_span(bk + "}", _DELIM, title=label))
|
|
192
|
+
elif m.group("other"):
|
|
193
|
+
# <div n="v">, <info lex="m"/> and friends: structural, kept visible but quiet.
|
|
194
|
+
out.append(_span(m.group("other"), _DELIM, title="structural markup"))
|
|
195
|
+
else:
|
|
196
|
+
out.append(_span("¦", _PIPE, "font-weight:700", title="headword / body separator"))
|
|
197
|
+
pos = m.end()
|
|
198
|
+
if pos < len(text):
|
|
199
|
+
out.append(plain(text[pos:]))
|
|
200
|
+
return (
|
|
201
|
+
'<div class="anatomy" style="background:#20242a;border-radius:6px;padding:12px 14px;'
|
|
202
|
+
'font:12.5px/1.9 Consolas,\'Cascadia Mono\',monospace;white-space:pre-wrap;'
|
|
203
|
+
'word-break:break-word">' + "".join(out) + "</div>"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def legend_html(parts=None, extra_chips=()):
|
|
208
|
+
"""A compact swatch legend for the part classes, for one place on the sheet.
|
|
209
|
+
|
|
210
|
+
``parts`` restricts (and orders) the classes shown — pass only the ones the
|
|
211
|
+
sheet's dictionary actually uses. ``extra_chips`` appends caller chips as
|
|
212
|
+
``(colour, label)`` pairs, for conventions this module does not own.
|
|
213
|
+
"""
|
|
214
|
+
keys = list(parts or PARTS)
|
|
215
|
+
chips = []
|
|
216
|
+
for key in keys:
|
|
217
|
+
colour, label, extra = PARTS[key]
|
|
218
|
+
chips.append(
|
|
219
|
+
'<span style="display:inline-block;margin:0 10px 4px 0;white-space:nowrap">'
|
|
220
|
+
'<span style="display:inline-block;width:10px;height:10px;border-radius:2px;'
|
|
221
|
+
'background:%s;margin-right:5px;vertical-align:baseline"></span>'
|
|
222
|
+
'<span style="color:%s;%s">%s</span></span>' % (colour, colour, extra, html.escape(label))
|
|
223
|
+
)
|
|
224
|
+
chips.append(
|
|
225
|
+
'<span style="display:inline-block;margin:0 10px 4px 0;white-space:nowrap">'
|
|
226
|
+
'<span style="display:inline-block;width:10px;height:10px;border-radius:2px;'
|
|
227
|
+
'background:rgba(86,182,194,.22);outline:1px solid #56b6c2;margin-right:5px"></span>'
|
|
228
|
+
'<span style="color:#56b6c2">цель перекрёстной ссылки</span></span>'
|
|
229
|
+
)
|
|
230
|
+
for colour, label in extra_chips:
|
|
231
|
+
chips.append(
|
|
232
|
+
'<span style="display:inline-block;margin:0 10px 4px 0;white-space:nowrap">'
|
|
233
|
+
'<span style="display:inline-block;width:10px;height:10px;border-radius:2px;'
|
|
234
|
+
'background:%s;margin-right:5px;vertical-align:baseline"></span>'
|
|
235
|
+
'<span style="color:%s">%s</span></span>' % (colour, colour, html.escape(label))
|
|
236
|
+
)
|
|
237
|
+
return '<div style="font-size:12px;line-height:1.9">' + "".join(chips) + "</div>"
|