nnnotes 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.
- nnnotes/__init__.py +7 -0
- nnnotes/__main__.py +4 -0
- nnnotes/acb.py +190 -0
- nnnotes/addressables.py +413 -0
- nnnotes/adv.py +243 -0
- nnnotes/advcommand.py +29 -0
- nnnotes/advmedia.py +178 -0
- nnnotes/advscene.py +113 -0
- nnnotes/advui.py +1154 -0
- nnnotes/advvideo.py +684 -0
- nnnotes/atoms/__init__.py +151 -0
- nnnotes/atoms/astc.py +52 -0
- nnnotes/atoms/gltf.py +290 -0
- nnnotes/atoms/mesh.py +289 -0
- nnnotes/atoms/png.py +28 -0
- nnnotes/atoms/reader.py +269 -0
- nnnotes/atoms/sniff.py +198 -0
- nnnotes/atoms/sprite.py +236 -0
- nnnotes/atoms/texture.py +80 -0
- nnnotes/cache.py +289 -0
- nnnotes/catalog.py +373 -0
- nnnotes/catalogdb.py +457 -0
- nnnotes/census.py +276 -0
- nnnotes/cli.py +744 -0
- nnnotes/cli_assets.py +1539 -0
- nnnotes/config.py +238 -0
- nnnotes/contract.py +497 -0
- nnnotes/cri.py +371 -0
- nnnotes/crikey.py +71 -0
- nnnotes/crilips.py +265 -0
- nnnotes/cristages.py +558 -0
- nnnotes/deckdata.py +462 -0
- nnnotes/export.py +1462 -0
- nnnotes/gameapi.py +318 -0
- nnnotes/jsonio.py +98 -0
- nnnotes/languages.py +44 -0
- nnnotes/layout.py +596 -0
- nnnotes/link.py +418 -0
- nnnotes/live.py +115 -0
- nnnotes/live2d.py +235 -0
- nnnotes/liveaudio.py +331 -0
- nnnotes/livenotes.py +291 -0
- nnnotes/liveoptions.py +250 -0
- nnnotes/livescene.py +450 -0
- nnnotes/liveui.py +328 -0
- nnnotes/master.py +251 -0
- nnnotes/modelstages.py +424 -0
- nnnotes/motion.py +131 -0
- nnnotes/objexport.py +1564 -0
- nnnotes/orchestrate.py +630 -0
- nnnotes/plan.py +218 -0
- nnnotes/player.py +392 -0
- nnnotes/room.py +391 -0
- nnnotes/score.py +1139 -0
- nnnotes/shader.py +217 -0
- nnnotes/spot.py +201 -0
- nnnotes/stages.py +285 -0
- nnnotes/store.py +518 -0
- nnnotes/story.py +142 -0
- nnnotes/storyfonts.py +1258 -0
- nnnotes/storyhost.py +1190 -0
- nnnotes/storysite.py +662 -0
- nnnotes/storyvoices.py +102 -0
- nnnotes/textstyle.py +355 -0
- nnnotes/tmpfont.py +636 -0
- nnnotes/typetrees/6000.3.12f1.json +19 -0
- nnnotes/unity.py +368 -0
- nnnotes/viewrules.json +441 -0
- nnnotes/views.py +966 -0
- nnnotes/voices.py +1363 -0
- nnnotes/web.py +1624 -0
- nnnotes/webaudio.py +14 -0
- nnnotes/webmodel.py +397 -0
- nnnotes-0.1.0.dist-info/METADATA +200 -0
- nnnotes-0.1.0.dist-info/RECORD +79 -0
- nnnotes-0.1.0.dist-info/WHEEL +5 -0
- nnnotes-0.1.0.dist-info/entry_points.txt +2 -0
- nnnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
- nnnotes-0.1.0.dist-info/top_level.txt +1 -0
nnnotes/stages.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""The stage protocol: how a stage describes its tasks and runs one, and the one function that runs a task.
|
|
2
|
+
|
|
3
|
+
A stage is a named, versioned unit of work applied to one subject at a time (a bundle, a content id, a model key,
|
|
4
|
+
a view, or the single subject of a global stage). It declares, for a subject:
|
|
5
|
+
|
|
6
|
+
subjects(env) the subjects it has, sorted (from the planning environment: catalog, censuses, ...)
|
|
7
|
+
normalize(params) the output-affecting parameters with every default filled (unknown ones refused)
|
|
8
|
+
inputs(subject, env) its inputs: roles, content ids, sizes, names, locators
|
|
9
|
+
uses(subject, env) the implementation ids of the atoms its task uses (only those)
|
|
10
|
+
context(subject, env) the context entries its task consumes (only those)
|
|
11
|
+
depends(subject, env) the task ids whose results it reads (the plan's edges)
|
|
12
|
+
estimate(subject, env, inputs) the expected cost (CPU seconds, peak bytes)
|
|
13
|
+
run(task, store) the work: reads the inputs through the store, writes the bytes of its artifacts
|
|
14
|
+
into the store, returns their records and one item per covered object
|
|
15
|
+
|
|
16
|
+
A stage that needs the output of a task that has not run raises Pending from inputs/uses/context (the environment
|
|
17
|
+
does it for it: Env.key / Env.result). While a task is described, `env.params` holds its normalized parameters, for
|
|
18
|
+
answers that depend on them (the atoms of the converters a parameter selects). `describe` turns the answers into a Task; `execute` runs a Task once and
|
|
19
|
+
commits its result (the store's action cache) after the artifacts' bytes are stored. The output of `run` must be a
|
|
20
|
+
function of the task's key parts: a change of what a stage writes is a change of its version (golden tests check).
|
|
21
|
+
Deterministic per-object failures are items (status `failed`, cached with the result); an exception out of `run`
|
|
22
|
+
fails the task, and nothing of it is cached.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import time
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from importlib import metadata
|
|
29
|
+
|
|
30
|
+
from . import contract
|
|
31
|
+
from .contract import Cost, IncompatibleTask, Input, Task
|
|
32
|
+
|
|
33
|
+
__all__ = ["Stage", "Env", "Pending", "Output", "Execution", "IncompatibleTask", "describe", "execute", "registry",
|
|
34
|
+
"golden", "golden_problems", "library_versions"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Pending(Exception):
|
|
38
|
+
"""A task cannot be described yet: it reads the result of `task`, which has not run (`failed`: which failed)."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, task: str, failed: bool = False):
|
|
41
|
+
super().__init__(f"{task} {'failed' if failed else 'has not run'}")
|
|
42
|
+
self.task, self.failed = task, failed
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Output:
|
|
47
|
+
"""What Stage.run returns: artifact records (their bytes already in the store) and one item per object."""
|
|
48
|
+
artifacts: list = field(default_factory=list)
|
|
49
|
+
items: list = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Stage:
|
|
53
|
+
"""Base of the stages. Subclasses set `name` and `version` and override what their subjects need; `ATOMS` is
|
|
54
|
+
every atom the stage may use (atom name -> implementation id), the default answer of `uses`."""
|
|
55
|
+
name: str = ""
|
|
56
|
+
version: int = 0
|
|
57
|
+
after: tuple = () # stages that run before this one (their results feed its tasks)
|
|
58
|
+
ATOMS: dict = {}
|
|
59
|
+
PARAMS: dict = {} # parameter defaults; normalize fills them and refuses unknown names
|
|
60
|
+
|
|
61
|
+
def subjects(self, env: "Env") -> list[str]:
|
|
62
|
+
raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
def normalize(self, params: dict | None) -> dict:
|
|
65
|
+
params = dict(params or {})
|
|
66
|
+
unknown = sorted(set(params) - set(self.PARAMS))
|
|
67
|
+
if unknown:
|
|
68
|
+
raise ValueError(f"stage {self.name}: unknown parameters {', '.join(unknown)}")
|
|
69
|
+
return {**self.PARAMS, **params}
|
|
70
|
+
|
|
71
|
+
def inputs(self, subject: str, env: "Env") -> list[Input]:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
def uses(self, subject: str, env: "Env") -> dict:
|
|
75
|
+
return dict(self.ATOMS)
|
|
76
|
+
|
|
77
|
+
def context(self, subject: str, env: "Env") -> dict:
|
|
78
|
+
return {}
|
|
79
|
+
|
|
80
|
+
def depends(self, subject: str, env: "Env") -> list[str]:
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
def estimate(self, subject: str, env: "Env", inputs: list[Input]) -> Cost:
|
|
84
|
+
return Cost(0.0, sum(i.size for i in inputs))
|
|
85
|
+
|
|
86
|
+
def run(self, task: Task, store) -> Output:
|
|
87
|
+
raise NotImplementedError
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def registry(*stages: Stage) -> dict[str, Stage]:
|
|
91
|
+
"""{name: stage}; names must be unique."""
|
|
92
|
+
out: dict[str, Stage] = {}
|
|
93
|
+
for s in stages:
|
|
94
|
+
if not s.name or s.name in out:
|
|
95
|
+
raise ValueError(f"stage name {s.name!r} missing or repeated")
|
|
96
|
+
out[s.name] = s
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Env:
|
|
101
|
+
"""What a stage reads while it describes a task: named facts (catalog index, censuses, script table, ...) and
|
|
102
|
+
the tasks of the current plan or run with their keys. `key` / `result` raise Pending for a task that has not
|
|
103
|
+
run (or failed); a stage lets that propagate."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, store, facts: dict | None = None):
|
|
106
|
+
self.store = store
|
|
107
|
+
self.facts = dict(facts or {})
|
|
108
|
+
self.params: dict = {} # the normalized parameters of the task being described
|
|
109
|
+
self._keys: dict[str, str] = {} # task id -> key, for tasks whose result is in the store
|
|
110
|
+
self._open: dict[str, bool] = {} # task id -> failed?, for tasks without a result
|
|
111
|
+
|
|
112
|
+
def fact(self, name: str):
|
|
113
|
+
try:
|
|
114
|
+
return self.facts[name]
|
|
115
|
+
except KeyError:
|
|
116
|
+
raise KeyError(f"no fact {name!r} in the planning environment") from None
|
|
117
|
+
|
|
118
|
+
def done(self, tid: str, key: str) -> None:
|
|
119
|
+
self._open.pop(tid, None)
|
|
120
|
+
self._keys[tid] = key
|
|
121
|
+
|
|
122
|
+
def waiting(self, tid: str, failed: bool = False) -> None:
|
|
123
|
+
self._keys.pop(tid, None)
|
|
124
|
+
self._open[tid] = failed
|
|
125
|
+
|
|
126
|
+
def known(self, tid: str) -> bool:
|
|
127
|
+
return tid in self._keys or tid in self._open
|
|
128
|
+
|
|
129
|
+
def key(self, tid: str) -> str:
|
|
130
|
+
if tid in self._keys:
|
|
131
|
+
return self._keys[tid]
|
|
132
|
+
raise Pending(tid, self._open.get(tid, False))
|
|
133
|
+
|
|
134
|
+
def result(self, tid: str) -> dict:
|
|
135
|
+
doc = self.store.result(self.key(tid))
|
|
136
|
+
if doc is None:
|
|
137
|
+
raise Pending(tid)
|
|
138
|
+
return doc
|
|
139
|
+
|
|
140
|
+
def tasks(self, stage: str) -> list[str]:
|
|
141
|
+
"""The task ids of `stage` with a result, sorted."""
|
|
142
|
+
return sorted(t for t in self._keys if t.startswith(stage + ":"))
|
|
143
|
+
|
|
144
|
+
def missing(self, stage: str) -> list[str]:
|
|
145
|
+
"""The task ids of `stage` without a result (not run yet, or failed), sorted."""
|
|
146
|
+
return sorted(t for t in self._open if t.startswith(stage + ":"))
|
|
147
|
+
|
|
148
|
+
def pending(self, stage: str) -> list[str]:
|
|
149
|
+
"""The task ids of `stage` that have not run yet (a plan: they would run first), sorted. A stage that reads
|
|
150
|
+
every task of another raises Pending for these; failed ones it skips (their failure is reported)."""
|
|
151
|
+
return sorted(t for t, failed in self._open.items() if not failed and t.startswith(stage + ":"))
|
|
152
|
+
|
|
153
|
+
def artifact(self, tid: str, role: str) -> dict:
|
|
154
|
+
"""The record of the artifact "<tid>#<role>" of a task's result."""
|
|
155
|
+
aid = contract.artifact_id(tid, role)
|
|
156
|
+
for a in self.result(tid)["artifacts"]:
|
|
157
|
+
if a["id"] == aid:
|
|
158
|
+
return a
|
|
159
|
+
raise KeyError(f"result of {tid} has no artifact {aid}")
|
|
160
|
+
|
|
161
|
+
def input_of(self, tid: str, role: str, as_role: str | None = None) -> Input:
|
|
162
|
+
"""A task input made of the artifact "<tid>#<role>" (read from the store)."""
|
|
163
|
+
c = self.artifact(tid, role)["content"]
|
|
164
|
+
return Input(as_role or role, c["sha256"], c["size"], None, ({"kind": "store"},))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def describe(stage: Stage, subject: str, params: dict | None, env: Env) -> Task:
|
|
168
|
+
"""The task of `stage` for `subject` (raises Pending when an input is not known yet)."""
|
|
169
|
+
p = stage.normalize(params)
|
|
170
|
+
env.params = p
|
|
171
|
+
try:
|
|
172
|
+
ins = list(stage.inputs(subject, env))
|
|
173
|
+
return Task(stage.name, stage.version, subject, p, dict(stage.uses(subject, env)), tuple(ins),
|
|
174
|
+
dict(stage.context(subject, env)), stage.estimate(subject, env, ins))
|
|
175
|
+
finally:
|
|
176
|
+
env.params = {}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class Execution:
|
|
181
|
+
"""The outcome of execute: `status` hit (the result was in the store) or ran; the measured cost of a run."""
|
|
182
|
+
task: str
|
|
183
|
+
key: str
|
|
184
|
+
status: str
|
|
185
|
+
result_status: str
|
|
186
|
+
cost: dict | None = None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def execute(task: Task, store, stages: dict[str, Stage], *, force: bool = False) -> Execution:
|
|
190
|
+
"""Run `task` once: verify that this installation has its stage at its version and can read every input with
|
|
191
|
+
its content id, run the stage, store its artifacts' bytes, then commit the result (write-once; a result already
|
|
192
|
+
in the store is a hit unless `force`, which runs again and requires identical output). Exceptions fail the task
|
|
193
|
+
and leave no result. The measured cost (never part of a key) counts the CPU time of this process and of the child
|
|
194
|
+
processes the stage ran and waited for (childCpuSeconds; null where the platform does not report it)."""
|
|
195
|
+
stage = stages.get(task.stage)
|
|
196
|
+
if stage is None:
|
|
197
|
+
raise IncompatibleTask(f"task {task.id}: no stage {task.stage!r} in this installation")
|
|
198
|
+
if stage.version != task.version:
|
|
199
|
+
raise IncompatibleTask(f"task {task.id}: stage {task.stage} is version {stage.version} here, "
|
|
200
|
+
f"the task was described for version {task.version}")
|
|
201
|
+
if not force:
|
|
202
|
+
hit = store.result(task.key)
|
|
203
|
+
if hit is not None:
|
|
204
|
+
return Execution(task.id, task.key, "hit", hit["status"])
|
|
205
|
+
for i in task.inputs:
|
|
206
|
+
store.open_input(i)
|
|
207
|
+
children = children_cpu()
|
|
208
|
+
cpu, wall = time.process_time(), time.perf_counter()
|
|
209
|
+
out = stage.run(task, store)
|
|
210
|
+
doc = contract.result(task, out.artifacts, out.items)
|
|
211
|
+
store.commit(doc)
|
|
212
|
+
own = time.process_time() - cpu
|
|
213
|
+
child = None if children is None else max(0.0, children_cpu() - children)
|
|
214
|
+
cost = {"schema": contract.COST, "key": task.key, "task": task.id,
|
|
215
|
+
"cpuSeconds": round(own + (child or 0.0), 3), "wallSeconds": round(time.perf_counter() - wall, 3),
|
|
216
|
+
"childCpuSeconds": None if child is None else round(child, 3)}
|
|
217
|
+
return Execution(task.id, task.key, "ran", doc["status"], cost)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def children_cpu() -> float | None:
|
|
221
|
+
"""User + system CPU seconds of this process's finished and waited-for child processes (the external tools a
|
|
222
|
+
stage runs), where the platform reports them (POSIX getrusage); None elsewhere."""
|
|
223
|
+
try:
|
|
224
|
+
import resource
|
|
225
|
+
except ImportError:
|
|
226
|
+
return None
|
|
227
|
+
ru = resource.getrusage(resource.RUSAGE_CHILDREN)
|
|
228
|
+
return ru.ru_utime + ru.ru_stime
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ---------------------------------------------------------------- golden tests
|
|
232
|
+
def library_versions(dists) -> dict[str, str]:
|
|
233
|
+
"""{distribution: installed version} ("-" when not installed)."""
|
|
234
|
+
out = {}
|
|
235
|
+
for d in sorted(dists):
|
|
236
|
+
try:
|
|
237
|
+
out[d] = metadata.version(d)
|
|
238
|
+
except metadata.PackageNotFoundError:
|
|
239
|
+
out[d] = "-"
|
|
240
|
+
return out
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def golden(stage: Stage, fixtures: dict, store, libraries=()) -> dict:
|
|
244
|
+
"""The golden record of a stage: its version, the atoms its fixtures use, the versions of `libraries`, and per
|
|
245
|
+
fixture the key and the sha256 of the canonical result. `fixtures`: {name: callable(store) -> Task} (each
|
|
246
|
+
builds its inputs into the store)."""
|
|
247
|
+
atoms, out = {}, {}
|
|
248
|
+
for name in sorted(fixtures):
|
|
249
|
+
task = fixtures[name](store)
|
|
250
|
+
if task.stage != stage.name:
|
|
251
|
+
raise ValueError(f"fixture {name} is a task of {task.stage}, not {stage.name}")
|
|
252
|
+
execute(task, store, {stage.name: stage}, force=True)
|
|
253
|
+
doc = store.result(task.key)
|
|
254
|
+
atoms.update(task.atoms)
|
|
255
|
+
out[name] = {"key": task.key, "result": contract.sha256(contract.encode(doc))}
|
|
256
|
+
return {"stage": stage.name, "version": stage.version, "atoms": dict(sorted(atoms.items())),
|
|
257
|
+
"libraries": library_versions(libraries), "fixtures": out}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def golden_problems(expected: dict, actual: dict, stage: Stage | None = None) -> list[str]:
|
|
261
|
+
"""Why `actual` (golden()) does not match the committed `expected`; [] when it does. An output change without a
|
|
262
|
+
version bump is the error the golden test exists for; a version bump requires a regenerated golden record.
|
|
263
|
+
With `stage`, every atom it declares must be used by some fixture."""
|
|
264
|
+
problems = []
|
|
265
|
+
if expected.get("version") != actual["version"]:
|
|
266
|
+
problems.append(f"{actual['stage']}: version {actual['version']}, golden record has "
|
|
267
|
+
f"{expected.get('version')}: regenerate the golden record")
|
|
268
|
+
if expected.get("atoms") != actual["atoms"]:
|
|
269
|
+
problems.append(f"{actual['stage']}: atoms {actual['atoms']} differ from the golden record's "
|
|
270
|
+
f"{expected.get('atoms')}")
|
|
271
|
+
exp, act = expected.get("fixtures", {}), actual["fixtures"]
|
|
272
|
+
for name in sorted(set(exp) | set(act)):
|
|
273
|
+
if name not in act:
|
|
274
|
+
problems.append(f"{actual['stage']}: fixture {name} missing")
|
|
275
|
+
elif name not in exp:
|
|
276
|
+
problems.append(f"{actual['stage']}: fixture {name} has no golden entry")
|
|
277
|
+
elif exp[name] != act[name]:
|
|
278
|
+
what = "output" if exp[name].get("key") == act[name]["key"] else "task key"
|
|
279
|
+
bump = " without a version bump" if expected.get("version") == actual["version"] else ""
|
|
280
|
+
problems.append(f"{actual['stage']}: fixture {name}: {what} changed{bump}")
|
|
281
|
+
if stage is not None:
|
|
282
|
+
unused = sorted(set(stage.ATOMS) - set(actual["atoms"]))
|
|
283
|
+
if unused:
|
|
284
|
+
problems.append(f"{actual['stage']}: no fixture uses {', '.join(unused)}")
|
|
285
|
+
return problems
|