csl-pyutil 0.24.1__py3-none-any.whl
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/__init__.py +68 -0
- csl_pyutil/anatomy.py +237 -0
- csl_pyutil/evidence.py +512 -0
- csl_pyutil/integrity_tripwire.py +679 -0
- csl_pyutil/review_sheet.py +3458 -0
- csl_pyutil-0.24.1.dist-info/METADATA +15 -0
- csl_pyutil-0.24.1.dist-info/RECORD +10 -0
- csl_pyutil-0.24.1.dist-info/WHEEL +5 -0
- csl_pyutil-0.24.1.dist-info/licenses/LICENSE +21 -0
- csl_pyutil-0.24.1.dist-info/top_level.txt +1 -0
csl_pyutil/__init__.py
ADDED
|
@@ -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"]
|
csl_pyutil/anatomy.py
ADDED
|
@@ -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>"
|