quirework 0.1.0__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.
- quirework/__init__.py +3 -0
- quirework/__main__.py +3 -0
- quirework/cli.py +431 -0
- quirework/core/__init__.py +1 -0
- quirework/core/artifact_paths.py +86 -0
- quirework/core/checks.py +235 -0
- quirework/core/frontmatter.py +83 -0
- quirework/core/globs.py +45 -0
- quirework/core/pinned.py +169 -0
- quirework/core/usage.py +604 -0
- quirework/delivery/__init__.py +1 -0
- quirework/delivery/artifacts.py +210 -0
- quirework/delivery/text.py +166 -0
- quirework/manuscript/__init__.py +1 -0
- quirework/manuscript/backfill.py +1972 -0
- quirework/manuscript/editions.py +111 -0
- quirework/manuscript/footprint.py +95 -0
- quirework/manuscript/formats.py +208 -0
- quirework/manuscript/lifecycle.py +191 -0
- quirework/manuscript/notes.py +2301 -0
- quirework/manuscript/pages.py +284 -0
- quirework/manuscript/quarto.py +416 -0
- quirework/manuscript/quotes.py +512 -0
- quirework/manuscript/rename.py +309 -0
- quirework/manuscript/sources.py +286 -0
- quirework/manuscript/trail.py +1875 -0
- quirework/manuscript/units.py +416 -0
- quirework/project.py +645 -0
- quirework/provenance/__init__.py +1 -0
- quirework/provenance/agyfilter.py +436 -0
- quirework/provenance/backup/__init__.py +469 -0
- quirework/provenance/backup/base.py +290 -0
- quirework/provenance/backup/command.py +107 -0
- quirework/provenance/backup/directory.py +142 -0
- quirework/provenance/backup/git.py +150 -0
- quirework/provenance/backup/rclone.py +153 -0
- quirework/provenance/capture/__init__.py +1 -0
- quirework/provenance/capture/commands.py +28 -0
- quirework/provenance/capture/extract_artifacts.py +110 -0
- quirework/provenance/capture/extract_files.py +118 -0
- quirework/provenance/capture/fetch_conversation.py +127 -0
- quirework/provenance/capture/fetch_gemini.py +149 -0
- quirework/provenance/capture/render_transcript.py +126 -0
- quirework/provenance/capture/transform.py +22 -0
- quirework/provenance/catalog.py +733 -0
- quirework/provenance/ccfilter.py +1126 -0
- quirework/provenance/codexfilter.py +883 -0
- quirework/provenance/coldread.py +1731 -0
- quirework/provenance/curate.py +67 -0
- quirework/provenance/humaninput.py +811 -0
- quirework/provenance/ingest.py +31 -0
- quirework/provenance/ingestion/__init__.py +1 -0
- quirework/provenance/ingestion/agy.py +192 -0
- quirework/provenance/ingestion/claude.py +493 -0
- quirework/provenance/ingestion/codex.py +296 -0
- quirework/provenance/lineage.py +148 -0
- quirework/provenance/metrics.py +1247 -0
- quirework/provenance/rulings.py +478 -0
- quirework/provenance/scope.py +73 -0
- quirework/provenance/search.py +455 -0
- quirework/provenance/semantic.py +475 -0
- quirework/provenance/show.py +1213 -0
- quirework/provenance/store.py +1271 -0
- quirework/provenance/trees.py +133 -0
- quirework/provenance/worktrees.py +199 -0
- quirework/providers/__init__.py +1 -0
- quirework/providers/blind.py +2192 -0
- quirework/providers/canary.py +685 -0
- quirework/providers/catalog.py +44 -0
- quirework/providers/claude_login.py +92 -0
- quirework/providers/exits.py +196 -0
- quirework/providers/harness.py +830 -0
- quirework/providers/images.py +92 -0
- quirework/providers/models.py +1284 -0
- quirework/providers/quota.py +588 -0
- quirework/providers/reads.py +474 -0
- quirework/providers/replies.py +175 -0
- quirework/publish/__init__.py +1 -0
- quirework/publish/backends.py +112 -0
- quirework/publish/bookstage.py +1086 -0
- quirework/publish/build.py +60 -0
- quirework/publish/editions.py +48 -0
- quirework/publish/epubcheck.py +287 -0
- quirework/publish/experiment.py +1097 -0
- quirework/publish/figfonts.py +234 -0
- quirework/publish/fignumbers.py +293 -0
- quirework/publish/figures.py +729 -0
- quirework/publish/folios.py +62 -0
- quirework/publish/fonts.py +307 -0
- quirework/publish/graybuild.py +73 -0
- quirework/publish/hugo.py +117 -0
- quirework/publish/kindle.js +411 -0
- quirework/publish/kindle.py +1393 -0
- quirework/publish/panelkit.py +918 -0
- quirework/publish/plans.py +651 -0
- quirework/publish/proof.py +792 -0
- quirework/publish/redpen.py +1654 -0
- quirework/publish/render.py +942 -0
- quirework/publish/targets.py +216 -0
- quirework/publish/variant.py +918 -0
- quirework/runs/__init__.py +1 -0
- quirework/runs/__main__.py +15 -0
- quirework/runs/commands.py +278 -0
- quirework/runs/coverage.py +86 -0
- quirework/runs/engine.py +2143 -0
- quirework/runs/feed.py +853 -0
- quirework/runs/observe.py +379 -0
- quirework/runs/storage.py +155 -0
- quirework/settings.py +492 -0
- quirework/sops/__init__.py +1 -0
- quirework/sops/catalog.py +386 -0
- quirework/sops/commands.py +567 -0
- quirework/sops/composition.py +382 -0
- quirework/sops/instruments.py +184 -0
- quirework/sops/personas.py +296 -0
- quirework/sops/review.py +2568 -0
- quirework/sops/room.py +2791 -0
- quirework/sops/roundtable.py +2578 -0
- quirework/sops/sections.py +169 -0
- quirework/sops/workshop.py +1629 -0
- quirework/templates/backup.toml +39 -0
- quirework/templates/book.toml +88 -0
- quirework/templates/run.toml +32 -0
- quirework/templates/site.toml +90 -0
- quirework/templates/tells.toml +64 -0
- quirework/templates/terminology.toml +20 -0
- quirework/templates/tmpfiles.conf +42 -0
- quirework/testing.py +345 -0
- quirework/text/__init__.py +1 -0
- quirework/text/jargon.py +545 -0
- quirework/text/mdwrap.py +1001 -0
- quirework/text/tells.py +547 -0
- quirework-0.1.0.dist-info/METADATA +444 -0
- quirework-0.1.0.dist-info/RECORD +138 -0
- quirework-0.1.0.dist-info/WHEEL +5 -0
- quirework-0.1.0.dist-info/entry_points.txt +2 -0
- quirework-0.1.0.dist-info/licenses/LICENSE +21 -0
- quirework-0.1.0.dist-info/top_level.txt +1 -0
quirework/settings.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Every setting Quirework reads, and every key of the policy files it reads.
|
|
2
|
+
|
|
3
|
+
A setting a project leaves undeclared takes its default and says nothing,
|
|
4
|
+
which is how an optional setting should behave and why a new one is easy to
|
|
5
|
+
miss: a project that does not know the key exists never learns of it. So
|
|
6
|
+
the keys are listed here, and `quirework config keys` sets them against
|
|
7
|
+
what a project declares: each known key declared or not, and each declared
|
|
8
|
+
key Quirework does not know, which is usually a misspelling. A key the
|
|
9
|
+
project's own code reads is listed by the project under `[extensions]`, and
|
|
10
|
+
then counts as its own rather than unknown.
|
|
11
|
+
|
|
12
|
+
A key is written dotted, as `quirework.toml` nests it. A `*` stands for one
|
|
13
|
+
name the project chooses, such as an edition's kind or a reader preset's
|
|
14
|
+
name. A setting that names a policy file has the keys that file may hold in
|
|
15
|
+
`POLICIES`, written the same way from the top of the file.
|
|
16
|
+
|
|
17
|
+
This module names keys; `docs/configuration.md` says what each one means.
|
|
18
|
+
Quirework's own suite records every key the code reads and fails on one
|
|
19
|
+
that is not listed here (`recording`), so the list cannot fall behind.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# The keys of [publish] a publish target's own table may also declare: a
|
|
27
|
+
# target is read as its [publish], each key it names replacing the shared one.
|
|
28
|
+
_PUBLISH = (
|
|
29
|
+
"backend",
|
|
30
|
+
"build",
|
|
31
|
+
"figure_checks",
|
|
32
|
+
"figure_fonts",
|
|
33
|
+
"figure_type_floor_pt",
|
|
34
|
+
"figures",
|
|
35
|
+
"grayscale_transform",
|
|
36
|
+
"layout_policy",
|
|
37
|
+
"render_policy",
|
|
38
|
+
"site",
|
|
39
|
+
"typst_template",
|
|
40
|
+
"variant_policy",
|
|
41
|
+
"variants",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
SETTINGS: tuple[str, ...] = (
|
|
45
|
+
"schema",
|
|
46
|
+
"checks.command",
|
|
47
|
+
# The keys a project's own code reads, which Quirework's never does.
|
|
48
|
+
"extensions.policy_keys.*",
|
|
49
|
+
"extensions.settings",
|
|
50
|
+
*(
|
|
51
|
+
f"manuscript.{key}"
|
|
52
|
+
for key in (
|
|
53
|
+
"chapter_policy",
|
|
54
|
+
"chapters",
|
|
55
|
+
"collections.*.order",
|
|
56
|
+
"collections.*.pages",
|
|
57
|
+
"companion_anchors",
|
|
58
|
+
"companion_dirs",
|
|
59
|
+
"format",
|
|
60
|
+
"ideas",
|
|
61
|
+
"marks",
|
|
62
|
+
"noncanonical_roots",
|
|
63
|
+
"pages",
|
|
64
|
+
"quarto",
|
|
65
|
+
"references",
|
|
66
|
+
"references_public",
|
|
67
|
+
"review_notes",
|
|
68
|
+
"revision_field",
|
|
69
|
+
"source",
|
|
70
|
+
"source_copies",
|
|
71
|
+
"sources",
|
|
72
|
+
"spec",
|
|
73
|
+
"spec_public",
|
|
74
|
+
"spec_trail",
|
|
75
|
+
"spec_trail_leads",
|
|
76
|
+
"unit_name",
|
|
77
|
+
"unit_spec",
|
|
78
|
+
)
|
|
79
|
+
),
|
|
80
|
+
"project.cache",
|
|
81
|
+
"project.name",
|
|
82
|
+
# Documents the project's private space; nothing reads it.
|
|
83
|
+
"project.private",
|
|
84
|
+
"project.scratchpad",
|
|
85
|
+
*(
|
|
86
|
+
f"provenance.{key}"
|
|
87
|
+
for key in (
|
|
88
|
+
"coldread",
|
|
89
|
+
"coldread_policy",
|
|
90
|
+
"enabled",
|
|
91
|
+
"human_input",
|
|
92
|
+
"metrics_policy",
|
|
93
|
+
"redaction_markers",
|
|
94
|
+
"redaction_policy",
|
|
95
|
+
"redpen",
|
|
96
|
+
"rendered",
|
|
97
|
+
"rulings",
|
|
98
|
+
"reports",
|
|
99
|
+
"root",
|
|
100
|
+
"store",
|
|
101
|
+
"store_dir",
|
|
102
|
+
)
|
|
103
|
+
),
|
|
104
|
+
# Per runner: claude, codex or agy.
|
|
105
|
+
"providers.*.account",
|
|
106
|
+
"providers.*.state",
|
|
107
|
+
"providers.agy.sessions",
|
|
108
|
+
"providers.canary_models.*",
|
|
109
|
+
"providers.claude.gateway_env",
|
|
110
|
+
"providers.claude.models",
|
|
111
|
+
"providers.claude.sessions",
|
|
112
|
+
"providers.codex.catalog",
|
|
113
|
+
"providers.isolation_policy",
|
|
114
|
+
*(f"publish.{key}" for key in _PUBLISH),
|
|
115
|
+
# Where a reader's Kindle notes are kept; one place for the whole
|
|
116
|
+
# project, so no publish target overlays it.
|
|
117
|
+
"publish.reader_notes",
|
|
118
|
+
"publish.targets.*.copy",
|
|
119
|
+
"publish.targets.*.needs",
|
|
120
|
+
*(f"publish.targets.*.{key}" for key in _PUBLISH),
|
|
121
|
+
"runs.evidence_roots",
|
|
122
|
+
"runs.reviews",
|
|
123
|
+
"runs.store",
|
|
124
|
+
"sops.catalog",
|
|
125
|
+
"sops.default_reader",
|
|
126
|
+
"sops.instruments",
|
|
127
|
+
"sops.operator_guide",
|
|
128
|
+
"sops.panels",
|
|
129
|
+
"sops.persona_policy",
|
|
130
|
+
"sops.personas",
|
|
131
|
+
"sops.reader_driver",
|
|
132
|
+
*(
|
|
133
|
+
f"sops.reader_presets.*.{key}"
|
|
134
|
+
for key in ("ask", "delivery", "include_matter", "instrument", "wrap_ask")
|
|
135
|
+
),
|
|
136
|
+
"sops.review_policy",
|
|
137
|
+
"sops.root",
|
|
138
|
+
"sops.roster",
|
|
139
|
+
"sops.skill_prefix",
|
|
140
|
+
"text.classifier",
|
|
141
|
+
"text.modes",
|
|
142
|
+
"text.terminology",
|
|
143
|
+
"text.tells_policy",
|
|
144
|
+
"usage.enabled",
|
|
145
|
+
"usage.store",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
POLICIES: dict[str, tuple[str, ...]] = {
|
|
149
|
+
"manuscript.chapter_policy": ("metadata_layers",),
|
|
150
|
+
"provenance.coldread_policy": ("cli_model", "doc_dirs", "doc_prefixes", "model"),
|
|
151
|
+
"provenance.metrics_policy": (
|
|
152
|
+
"canonical_dir",
|
|
153
|
+
"essay_paths",
|
|
154
|
+
"output_counts.*",
|
|
155
|
+
"source_of_truth",
|
|
156
|
+
),
|
|
157
|
+
"provenance.redaction_policy": ("markers",),
|
|
158
|
+
"providers.isolation_policy": ("mount_markers", "project_markers"),
|
|
159
|
+
"publish.layout_policy": ("typst_epilogues.*", "typst_prologues.*"),
|
|
160
|
+
"publish.render_policy": (
|
|
161
|
+
"cover_source",
|
|
162
|
+
"epub_style_headers",
|
|
163
|
+
"folio_sides",
|
|
164
|
+
"noparts_drops",
|
|
165
|
+
"print_drops",
|
|
166
|
+
"proof_name",
|
|
167
|
+
"proof_switch",
|
|
168
|
+
"proof_title",
|
|
169
|
+
"reader_metadata.*",
|
|
170
|
+
"revision_metadata",
|
|
171
|
+
),
|
|
172
|
+
"publish.variant_policy": (
|
|
173
|
+
"blind_tells",
|
|
174
|
+
"default_effort",
|
|
175
|
+
"default_harness",
|
|
176
|
+
"default_model",
|
|
177
|
+
"document_intro",
|
|
178
|
+
"prompt_intro",
|
|
179
|
+
*(f"kinds.*.{key}" for key in ("effort", "harness", "model", "omit", "shape")),
|
|
180
|
+
),
|
|
181
|
+
"sops.persona_policy": ("default",),
|
|
182
|
+
"sops.review_policy": (
|
|
183
|
+
"frame",
|
|
184
|
+
"hidden",
|
|
185
|
+
"own_output",
|
|
186
|
+
"procedure_frame",
|
|
187
|
+
"reviewable",
|
|
188
|
+
),
|
|
189
|
+
"text.terminology": ("allow", "exclude_names", "retired", "scope_glob", "terms.*"),
|
|
190
|
+
"text.tells_policy": ("contrast", "exclude_names", "phrases.*", "scope_glob"),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _parts(key: str) -> tuple[str, ...]:
|
|
195
|
+
return tuple(key.split(".")) if key else ()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _matches(pattern: tuple[str, ...], path: tuple[str, ...]) -> bool:
|
|
199
|
+
return len(pattern) == len(path) and all(
|
|
200
|
+
p == "*" or p == q for p, q in zip(pattern, path)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _leads_to(pattern: tuple[str, ...], path: tuple[str, ...]) -> bool:
|
|
205
|
+
"""Whether ``path`` is ``pattern`` or a table on the way to it."""
|
|
206
|
+
return len(path) <= len(pattern) and _matches(pattern[: len(path)], path)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def known(path, patterns) -> bool:
|
|
210
|
+
"""Whether ``patterns`` name this key, a table on the way to one, or a
|
|
211
|
+
name below a pattern that ends in ``*``, whose contents are the project's."""
|
|
212
|
+
path = _parts(path) if isinstance(path, str) else tuple(path)
|
|
213
|
+
for pattern in map(_parts, patterns):
|
|
214
|
+
if _leads_to(pattern, path):
|
|
215
|
+
return True
|
|
216
|
+
if pattern[-1:] == ("*",) and _matches(pattern, path[: len(pattern)]):
|
|
217
|
+
return True
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# --------------------------------------------------------------------------- #
|
|
222
|
+
# Recording what the code reads, for Quirework's own suite
|
|
223
|
+
# --------------------------------------------------------------------------- #
|
|
224
|
+
|
|
225
|
+
_seen: set[tuple[str, str]] | None = None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def recording() -> None:
|
|
229
|
+
"""Start noting every key read through a project, for `unregistered`."""
|
|
230
|
+
global _seen
|
|
231
|
+
_seen = set()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def unregistered() -> list[str]:
|
|
235
|
+
"""The keys read since `recording` that this module does not list."""
|
|
236
|
+
found = []
|
|
237
|
+
for where, key in sorted(_seen or ()):
|
|
238
|
+
patterns = SETTINGS if where == "" else POLICIES.get(where, ())
|
|
239
|
+
if not known(key, patterns):
|
|
240
|
+
found.append(f"{where}: {key}" if where else key)
|
|
241
|
+
return found
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def note(where: str, key: str) -> None:
|
|
245
|
+
if _seen is not None:
|
|
246
|
+
_seen.add((where, key))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def watched(value, where: str, prefix: str):
|
|
250
|
+
"""``value``, made to note the keys read out of it, while recording."""
|
|
251
|
+
if _seen is None or not isinstance(value, dict):
|
|
252
|
+
return value
|
|
253
|
+
return Watched(value, where, prefix)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class Watched(dict):
|
|
257
|
+
"""A table that notes each key the code names when reading it.
|
|
258
|
+
|
|
259
|
+
Only a named read counts: iterating a table reads names the project
|
|
260
|
+
chose, not keys Quirework knows. A table reached inside it, by name or
|
|
261
|
+
by iterating, is watched too, under the longer prefix, and replaces the
|
|
262
|
+
plain one in place, so writing through it is seen by the next read.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(self, value, where, prefix):
|
|
266
|
+
super().__init__(value)
|
|
267
|
+
self._where, self._prefix = where, prefix
|
|
268
|
+
|
|
269
|
+
def _named(self, key):
|
|
270
|
+
note(self._where, f"{self._prefix}.{key}" if self._prefix else str(key))
|
|
271
|
+
|
|
272
|
+
def _inner(self, key):
|
|
273
|
+
value = super().__getitem__(key)
|
|
274
|
+
if isinstance(value, dict) and not isinstance(value, Watched):
|
|
275
|
+
prefix = f"{self._prefix}.{key}" if self._prefix else str(key)
|
|
276
|
+
value = Watched(value, self._where, prefix)
|
|
277
|
+
super().__setitem__(key, value)
|
|
278
|
+
return value
|
|
279
|
+
|
|
280
|
+
def __getitem__(self, key):
|
|
281
|
+
self._named(key)
|
|
282
|
+
return self._inner(key)
|
|
283
|
+
|
|
284
|
+
def get(self, key, default=None):
|
|
285
|
+
self._named(key)
|
|
286
|
+
if super().__contains__(key):
|
|
287
|
+
return self._inner(key)
|
|
288
|
+
# A table the project left out still has its keys read, from the
|
|
289
|
+
# caller's default, and those reads count as much as any.
|
|
290
|
+
prefix = f"{self._prefix}.{key}" if self._prefix else str(key)
|
|
291
|
+
return watched(default, self._where, prefix)
|
|
292
|
+
|
|
293
|
+
def __contains__(self, key):
|
|
294
|
+
self._named(key)
|
|
295
|
+
return super().__contains__(key)
|
|
296
|
+
|
|
297
|
+
def setdefault(self, key, default=None):
|
|
298
|
+
self._named(key)
|
|
299
|
+
if not super().__contains__(key):
|
|
300
|
+
super().__setitem__(key, default)
|
|
301
|
+
return self._inner(key)
|
|
302
|
+
|
|
303
|
+
def pop(self, key, *default):
|
|
304
|
+
self._named(key)
|
|
305
|
+
return super().pop(key, *default)
|
|
306
|
+
|
|
307
|
+
def values(self):
|
|
308
|
+
return [self._inner(key) for key in self]
|
|
309
|
+
|
|
310
|
+
def items(self):
|
|
311
|
+
return [(key, self._inner(key)) for key in self]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# --------------------------------------------------------------------------- #
|
|
315
|
+
# What a project declares, against what Quirework knows
|
|
316
|
+
# --------------------------------------------------------------------------- #
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _declared(table, patterns, prefix=()):
|
|
320
|
+
"""Every declared key in ``table``, as a tuple of names.
|
|
321
|
+
|
|
322
|
+
A table is walked into until a key a pattern names exactly, since that
|
|
323
|
+
setting's value may itself be a table, or a name below a pattern ending
|
|
324
|
+
in ``*``, whose value is the project's own."""
|
|
325
|
+
for name, value in table.items():
|
|
326
|
+
path = (*prefix, name)
|
|
327
|
+
whole = any(_matches(_parts(p), path) for p in patterns)
|
|
328
|
+
if isinstance(value, dict) and value and not whole:
|
|
329
|
+
yield from _declared(value, patterns, path)
|
|
330
|
+
else:
|
|
331
|
+
yield path
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _patterns(value, name):
|
|
335
|
+
from .project import ConfigurationError
|
|
336
|
+
|
|
337
|
+
if not isinstance(value, list) or not all(
|
|
338
|
+
isinstance(v, str) and v.strip() for v in value
|
|
339
|
+
):
|
|
340
|
+
raise ConfigurationError(f"{name} must be a list of dotted keys")
|
|
341
|
+
return tuple(value)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def own(project) -> tuple[tuple[str, ...], dict[str, tuple[str, ...]]]:
|
|
345
|
+
"""The keys ``project``'s own code reads, which Quirework's does not.
|
|
346
|
+
|
|
347
|
+
An extension a project installs may read a setting of its own, or a key
|
|
348
|
+
of its own in a policy file it shares with Quirework, and nothing here
|
|
349
|
+
can know either. So the project lists them: `extensions.settings` for
|
|
350
|
+
`quirework.toml`, dotted as Quirework's are, and `extensions.policy_keys`,
|
|
351
|
+
one list per policy setting, written from the top of that file. Only a
|
|
352
|
+
policy file Quirework reads takes a list, since a file nothing here reads
|
|
353
|
+
is never set against Quirework's keys in the first place."""
|
|
354
|
+
from .project import ConfigurationError
|
|
355
|
+
|
|
356
|
+
settings = _patterns(project.get("extensions.settings", []), "extensions.settings")
|
|
357
|
+
table = project.get("extensions.policy_keys", {})
|
|
358
|
+
if not isinstance(table, dict):
|
|
359
|
+
raise ConfigurationError(
|
|
360
|
+
"extensions.policy_keys must be a table: one list per policy setting"
|
|
361
|
+
)
|
|
362
|
+
policies = {}
|
|
363
|
+
for setting, value in table.items():
|
|
364
|
+
if setting not in POLICIES:
|
|
365
|
+
raise ConfigurationError(
|
|
366
|
+
f"extensions.policy_keys names {setting}, which is not a policy "
|
|
367
|
+
"file Quirework reads, so no key of it needs listing"
|
|
368
|
+
)
|
|
369
|
+
policies[setting] = _patterns(value, f"extensions.policy_keys.{setting}")
|
|
370
|
+
return settings, policies
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _compare(table, patterns, mine=()):
|
|
374
|
+
keys = list(_declared(table, (*patterns, *mine)))
|
|
375
|
+
|
|
376
|
+
def present(candidates):
|
|
377
|
+
return sorted(
|
|
378
|
+
p
|
|
379
|
+
for p in candidates
|
|
380
|
+
if any(_matches(_parts(p), k[: len(_parts(p))]) for k in keys)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
declared = present(patterns)
|
|
384
|
+
return {
|
|
385
|
+
"declared": declared,
|
|
386
|
+
"undeclared": sorted(set(patterns) - set(declared)),
|
|
387
|
+
"own": present(mine),
|
|
388
|
+
"unknown": sorted(
|
|
389
|
+
".".join(k) for k in keys if not known(k, patterns) and not known(k, mine)
|
|
390
|
+
),
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def report(project) -> dict:
|
|
395
|
+
"""Each known setting and policy key, declared by ``project`` or not, the
|
|
396
|
+
project's own keys it declares, and each it declares that nobody knows."""
|
|
397
|
+
own_settings, own_policies = own(project)
|
|
398
|
+
found = {
|
|
399
|
+
"settings": _compare(project.describe()["settings"], SETTINGS, own_settings)
|
|
400
|
+
}
|
|
401
|
+
policies = {}
|
|
402
|
+
for setting, patterns in sorted(POLICIES.items()):
|
|
403
|
+
if project.get(setting) is None:
|
|
404
|
+
policies[setting] = {
|
|
405
|
+
"file": None,
|
|
406
|
+
"declared": [],
|
|
407
|
+
"undeclared": sorted(patterns),
|
|
408
|
+
"own": [],
|
|
409
|
+
"unknown": [],
|
|
410
|
+
}
|
|
411
|
+
continue
|
|
412
|
+
data = project.data(setting, {})
|
|
413
|
+
policies[setting] = {
|
|
414
|
+
"file": str(project.path(setting)),
|
|
415
|
+
**_compare(
|
|
416
|
+
data if isinstance(data, dict) else {},
|
|
417
|
+
patterns,
|
|
418
|
+
own_policies.get(setting, ()),
|
|
419
|
+
),
|
|
420
|
+
}
|
|
421
|
+
found["policies"] = policies
|
|
422
|
+
return found
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _where(project, path) -> str:
|
|
426
|
+
"""A file as the project would name it: relative to its root when inside."""
|
|
427
|
+
path = Path(path)
|
|
428
|
+
root = project.root.resolve()
|
|
429
|
+
resolved = path.resolve()
|
|
430
|
+
return (
|
|
431
|
+
str(resolved.relative_to(root)) if resolved.is_relative_to(root) else str(path)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def unknown(project) -> list[str]:
|
|
436
|
+
"""Each declared key nothing reads, as a line naming its file.
|
|
437
|
+
|
|
438
|
+
A misspelled key is read as absent, so it takes its default and says
|
|
439
|
+
nothing, and so does a key a release retired. `config check` refuses a
|
|
440
|
+
project that declares one, in `quirework.toml`, in the local overlay or
|
|
441
|
+
in a policy file the project names, so the misspelling surfaces where
|
|
442
|
+
somebody asked whether the configuration is right. A key the project's
|
|
443
|
+
own code reads is not one of them once `[extensions]` lists it (`own`),
|
|
444
|
+
and a key under `[extensions]` other than its two lists is."""
|
|
445
|
+
found = report(project)
|
|
446
|
+
described = project.describe()
|
|
447
|
+
origins, fallback = described["origins"], described["configuration"]
|
|
448
|
+
lines = [
|
|
449
|
+
f"{_where(project, origins.get(key) or fallback)}: unknown setting {key}"
|
|
450
|
+
for key in found["settings"]["unknown"]
|
|
451
|
+
]
|
|
452
|
+
for setting, rows in found["policies"].items():
|
|
453
|
+
lines.extend(
|
|
454
|
+
f"{_where(project, rows['file'])}: unknown key {key} in {setting}"
|
|
455
|
+
for key in rows["unknown"]
|
|
456
|
+
)
|
|
457
|
+
return lines
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def show(project, *, undeclared_only=False) -> str:
|
|
461
|
+
"""`report` for a person: the settings first, then each policy file."""
|
|
462
|
+
found = report(project)
|
|
463
|
+
lines = []
|
|
464
|
+
|
|
465
|
+
def section(rows, indent):
|
|
466
|
+
kinds = (
|
|
467
|
+
("undeclared", "unknown")
|
|
468
|
+
if undeclared_only
|
|
469
|
+
else (
|
|
470
|
+
"declared",
|
|
471
|
+
"own",
|
|
472
|
+
"undeclared",
|
|
473
|
+
"unknown",
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
for kind in kinds:
|
|
477
|
+
for key in rows[kind]:
|
|
478
|
+
label = "unknown" if kind == "unknown" else kind
|
|
479
|
+
lines.append(f"{indent}{label:11} {key}")
|
|
480
|
+
|
|
481
|
+
lines.append("Settings (quirework.toml):")
|
|
482
|
+
section(found["settings"], " ")
|
|
483
|
+
for setting, rows in found["policies"].items():
|
|
484
|
+
where = rows["file"] or "not declared, so every key is undeclared"
|
|
485
|
+
lines.append(f"{setting} ({where}):")
|
|
486
|
+
section(rows, " ")
|
|
487
|
+
lines.append(
|
|
488
|
+
"An own key is one the project's own code reads, as [extensions] lists "
|
|
489
|
+
"it. An unknown key is one nothing reads, usually a misspelling. "
|
|
490
|
+
"docs/configuration.md explains every known one."
|
|
491
|
+
)
|
|
492
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Sops services."""
|