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.
Files changed (79) hide show
  1. nnnotes/__init__.py +7 -0
  2. nnnotes/__main__.py +4 -0
  3. nnnotes/acb.py +190 -0
  4. nnnotes/addressables.py +413 -0
  5. nnnotes/adv.py +243 -0
  6. nnnotes/advcommand.py +29 -0
  7. nnnotes/advmedia.py +178 -0
  8. nnnotes/advscene.py +113 -0
  9. nnnotes/advui.py +1154 -0
  10. nnnotes/advvideo.py +684 -0
  11. nnnotes/atoms/__init__.py +151 -0
  12. nnnotes/atoms/astc.py +52 -0
  13. nnnotes/atoms/gltf.py +290 -0
  14. nnnotes/atoms/mesh.py +289 -0
  15. nnnotes/atoms/png.py +28 -0
  16. nnnotes/atoms/reader.py +269 -0
  17. nnnotes/atoms/sniff.py +198 -0
  18. nnnotes/atoms/sprite.py +236 -0
  19. nnnotes/atoms/texture.py +80 -0
  20. nnnotes/cache.py +289 -0
  21. nnnotes/catalog.py +373 -0
  22. nnnotes/catalogdb.py +457 -0
  23. nnnotes/census.py +276 -0
  24. nnnotes/cli.py +744 -0
  25. nnnotes/cli_assets.py +1539 -0
  26. nnnotes/config.py +238 -0
  27. nnnotes/contract.py +497 -0
  28. nnnotes/cri.py +371 -0
  29. nnnotes/crikey.py +71 -0
  30. nnnotes/crilips.py +265 -0
  31. nnnotes/cristages.py +558 -0
  32. nnnotes/deckdata.py +462 -0
  33. nnnotes/export.py +1462 -0
  34. nnnotes/gameapi.py +318 -0
  35. nnnotes/jsonio.py +98 -0
  36. nnnotes/languages.py +44 -0
  37. nnnotes/layout.py +596 -0
  38. nnnotes/link.py +418 -0
  39. nnnotes/live.py +115 -0
  40. nnnotes/live2d.py +235 -0
  41. nnnotes/liveaudio.py +331 -0
  42. nnnotes/livenotes.py +291 -0
  43. nnnotes/liveoptions.py +250 -0
  44. nnnotes/livescene.py +450 -0
  45. nnnotes/liveui.py +328 -0
  46. nnnotes/master.py +251 -0
  47. nnnotes/modelstages.py +424 -0
  48. nnnotes/motion.py +131 -0
  49. nnnotes/objexport.py +1564 -0
  50. nnnotes/orchestrate.py +630 -0
  51. nnnotes/plan.py +218 -0
  52. nnnotes/player.py +392 -0
  53. nnnotes/room.py +391 -0
  54. nnnotes/score.py +1139 -0
  55. nnnotes/shader.py +217 -0
  56. nnnotes/spot.py +201 -0
  57. nnnotes/stages.py +285 -0
  58. nnnotes/store.py +518 -0
  59. nnnotes/story.py +142 -0
  60. nnnotes/storyfonts.py +1258 -0
  61. nnnotes/storyhost.py +1190 -0
  62. nnnotes/storysite.py +662 -0
  63. nnnotes/storyvoices.py +102 -0
  64. nnnotes/textstyle.py +355 -0
  65. nnnotes/tmpfont.py +636 -0
  66. nnnotes/typetrees/6000.3.12f1.json +19 -0
  67. nnnotes/unity.py +368 -0
  68. nnnotes/viewrules.json +441 -0
  69. nnnotes/views.py +966 -0
  70. nnnotes/voices.py +1363 -0
  71. nnnotes/web.py +1624 -0
  72. nnnotes/webaudio.py +14 -0
  73. nnnotes/webmodel.py +397 -0
  74. nnnotes-0.1.0.dist-info/METADATA +200 -0
  75. nnnotes-0.1.0.dist-info/RECORD +79 -0
  76. nnnotes-0.1.0.dist-info/WHEEL +5 -0
  77. nnnotes-0.1.0.dist-info/entry_points.txt +2 -0
  78. nnnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
  79. 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