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/config.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Settings from a TOML file, environment variables and command-line flags.
|
|
2
|
+
|
|
3
|
+
Sources, lowest to highest precedence:
|
|
4
|
+
|
|
5
|
+
1. the TOML file: `--config <file>`, else the file named by `NNNOTES_CONFIG`, else `./nnnotes.toml` when present;
|
|
6
|
+
2. environment variables `NNNOTES_<SECTION>_<KEY>`: the setting's dotted name upper-cased, dots and dashes as
|
|
7
|
+
underscores (`servers.tw.cdn` -> `NNNOTES_SERVERS_TW_CDN`, `bundle.nonce_seed` -> `NNNOTES_BUNDLE_NONCE_SEED`);
|
|
8
|
+
3. command-line flags.
|
|
9
|
+
|
|
10
|
+
No setting has a default and an empty value counts as unset. A command that needs a setting nobody gave stops with
|
|
11
|
+
a ConfigError naming the TOML key and the environment variable. Values are never printed, logged or put into an
|
|
12
|
+
error message. Relative paths in the TOML file are relative to the file's directory; relative paths from the
|
|
13
|
+
environment or the command line are relative to the working directory.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import shutil
|
|
20
|
+
import tomllib
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
ENV_PREFIX = "NNNOTES_"
|
|
24
|
+
ENV_CONFIG = "NNNOTES_CONFIG"
|
|
25
|
+
DEFAULT_FILE = "nnnotes.toml"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfigError(Exception):
|
|
29
|
+
"""A setting is missing or malformed (the message names the setting, never its value)."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def env_name(section: str, key: str) -> str:
|
|
33
|
+
return ENV_PREFIX + re.sub(r"[^A-Za-z0-9]", "_", f"{section}.{key}").upper()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def apk_missing(what: str) -> "ConfigError":
|
|
37
|
+
"""The error for data that is only in the game's APK when `[paths] apk` is not set."""
|
|
38
|
+
return ConfigError(f"{what} needs the game's APK: give base.apk as {describe('paths', 'apk', '--apk')}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def describe(section: str, key: str, flag: str | None = None) -> str:
|
|
42
|
+
"""Where a setting can be given: `[section] key` in the TOML file, the environment variable, the flag."""
|
|
43
|
+
where = f"`{key}` in the [{section}] table of the config file, the environment variable {env_name(section, key)}"
|
|
44
|
+
return where + (f" or {flag}" if flag else "")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Config:
|
|
48
|
+
"""Merged settings. `overrides`: {(section, key): value} from command-line flags (None = not given)."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, data: dict | None = None, base: Path | None = None, source: str | None = None,
|
|
51
|
+
environ: dict[str, str] | None = None, overrides: dict | None = None, flags: dict | None = None):
|
|
52
|
+
self._data = data or {}
|
|
53
|
+
self._base = base
|
|
54
|
+
self.source = source # the TOML file read, if any (a path, shown in messages)
|
|
55
|
+
env = os.environ if environ is None else environ
|
|
56
|
+
self._env = {k: v for k, v in env.items() if k.startswith(ENV_PREFIX) and k != ENV_CONFIG}
|
|
57
|
+
self._over = {k: v for k, v in (overrides or {}).items() if v not in (None, "")}
|
|
58
|
+
self._flags = dict(flags or {}) # {(section, key): "--flag"} for messages
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str: # never the values
|
|
61
|
+
return f"Config(source={self.source!r})"
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------- loading
|
|
64
|
+
@classmethod
|
|
65
|
+
def load(cls, path: str | Path | None = None, *, environ: dict[str, str] | None = None,
|
|
66
|
+
overrides: dict | None = None, flags: dict | None = None) -> "Config":
|
|
67
|
+
env = os.environ if environ is None else environ
|
|
68
|
+
if path:
|
|
69
|
+
file = Path(path)
|
|
70
|
+
if not file.is_file():
|
|
71
|
+
raise ConfigError(f"config file {file} (--config) not found")
|
|
72
|
+
elif env.get(ENV_CONFIG):
|
|
73
|
+
file = Path(env[ENV_CONFIG])
|
|
74
|
+
if not file.is_file():
|
|
75
|
+
raise ConfigError(f"config file {file} ({ENV_CONFIG}) not found")
|
|
76
|
+
else:
|
|
77
|
+
file = Path(DEFAULT_FILE)
|
|
78
|
+
if not file.is_file():
|
|
79
|
+
return cls(environ=env, overrides=overrides, flags=flags)
|
|
80
|
+
try:
|
|
81
|
+
data = tomllib.loads(file.read_text(encoding="utf-8"))
|
|
82
|
+
except tomllib.TOMLDecodeError as e:
|
|
83
|
+
raise ConfigError(f"config file {file}: invalid TOML ({e})") from None
|
|
84
|
+
return cls(data, base=file.resolve().parent, source=str(file), environ=env, overrides=overrides,
|
|
85
|
+
flags=flags)
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------- lookup
|
|
88
|
+
def _table(self, section: str) -> dict:
|
|
89
|
+
t = self._data
|
|
90
|
+
for part in section.split("."):
|
|
91
|
+
t = t.get(part) if isinstance(t, dict) else None
|
|
92
|
+
return t if isinstance(t, dict) else {}
|
|
93
|
+
|
|
94
|
+
def _raw(self, section: str, key: str):
|
|
95
|
+
"""(value, origin) with origin 'flag', 'env' or 'file'; (None, None) when unset."""
|
|
96
|
+
if (section, key) in self._over:
|
|
97
|
+
return self._over[(section, key)], "flag"
|
|
98
|
+
v = self._env.get(env_name(section, key))
|
|
99
|
+
if v:
|
|
100
|
+
return v, "env"
|
|
101
|
+
v = self._table(section).get(key)
|
|
102
|
+
if v in (None, "", []):
|
|
103
|
+
return None, None
|
|
104
|
+
return v, "file"
|
|
105
|
+
|
|
106
|
+
def has(self, section: str, key: str) -> bool:
|
|
107
|
+
return self._raw(section, key)[0] is not None
|
|
108
|
+
|
|
109
|
+
def origin(self, section: str, key: str) -> str | None:
|
|
110
|
+
"""Where a setting comes from: 'flag', 'env', 'file', or None when it is unset."""
|
|
111
|
+
return self._raw(section, key)[1]
|
|
112
|
+
|
|
113
|
+
def missing(self, section: str, key: str) -> ConfigError:
|
|
114
|
+
return ConfigError(f"setting {section}.{key} is not set: give it as "
|
|
115
|
+
f"{describe(section, key, self._flags.get((section, key)))}")
|
|
116
|
+
|
|
117
|
+
def _bad(self, section: str, key: str, what: str) -> ConfigError:
|
|
118
|
+
return ConfigError(f"setting {section}.{key}: {what}")
|
|
119
|
+
|
|
120
|
+
def get(self, section: str, key: str) -> str | None:
|
|
121
|
+
v, _ = self._raw(section, key)
|
|
122
|
+
if v is None:
|
|
123
|
+
return None
|
|
124
|
+
if not isinstance(v, str):
|
|
125
|
+
raise self._bad(section, key, "must be a string")
|
|
126
|
+
return v
|
|
127
|
+
|
|
128
|
+
def require(self, section: str, key: str) -> str:
|
|
129
|
+
v = self.get(section, key)
|
|
130
|
+
if v is None:
|
|
131
|
+
raise self.missing(section, key)
|
|
132
|
+
return v
|
|
133
|
+
|
|
134
|
+
def get_list(self, section: str, key: str) -> list[str]:
|
|
135
|
+
"""A list of strings (TOML array; comma-separated in the environment or on the command line)."""
|
|
136
|
+
v, origin = self._raw(section, key)
|
|
137
|
+
if v is None:
|
|
138
|
+
return []
|
|
139
|
+
if isinstance(v, str):
|
|
140
|
+
return [s.strip() for s in v.split(",") if s.strip()]
|
|
141
|
+
if not isinstance(v, list) or not all(isinstance(s, str) for s in v):
|
|
142
|
+
raise self._bad(section, key, "must be a list of strings")
|
|
143
|
+
return list(v)
|
|
144
|
+
|
|
145
|
+
def path(self, section: str, key: str) -> Path | None:
|
|
146
|
+
v, origin = self._raw(section, key)
|
|
147
|
+
if v is None:
|
|
148
|
+
return None
|
|
149
|
+
if not isinstance(v, (str, os.PathLike)):
|
|
150
|
+
raise self._bad(section, key, "must be a path string")
|
|
151
|
+
p = Path(v).expanduser()
|
|
152
|
+
if origin == "file" and not p.is_absolute() and self._base is not None:
|
|
153
|
+
p = self._base / p
|
|
154
|
+
return p
|
|
155
|
+
|
|
156
|
+
def require_path(self, section: str, key: str) -> Path:
|
|
157
|
+
p = self.path(section, key)
|
|
158
|
+
if p is None:
|
|
159
|
+
raise self.missing(section, key)
|
|
160
|
+
return p
|
|
161
|
+
|
|
162
|
+
def hex(self, section: str, key: str, size: int | None = None) -> bytes:
|
|
163
|
+
"""A required hex string as bytes (`size`: the exact byte length)."""
|
|
164
|
+
v = self.require(section, key)
|
|
165
|
+
s = v.strip()
|
|
166
|
+
if s[:2].lower() == "0x":
|
|
167
|
+
s = s[2:]
|
|
168
|
+
try:
|
|
169
|
+
b = bytes.fromhex(s)
|
|
170
|
+
except ValueError:
|
|
171
|
+
raise self._bad(section, key, "must be a hex string") from None
|
|
172
|
+
if size is not None and len(b) != size:
|
|
173
|
+
raise self._bad(section, key, f"must be {size} bytes ({2 * size} hex digits)")
|
|
174
|
+
return b
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------- servers
|
|
177
|
+
def regions(self) -> list[str]:
|
|
178
|
+
"""Region names: the [servers.<region>] tables and the regions given by NNNOTES_SERVERS_<REGION>_CDN."""
|
|
179
|
+
names = [k for k, v in self._table("servers").items() if isinstance(v, dict)]
|
|
180
|
+
for k in self._env:
|
|
181
|
+
m = re.fullmatch(ENV_PREFIX + r"SERVERS_([A-Z0-9_]+)_CDN", k)
|
|
182
|
+
if m and m.group(1).lower() not in names:
|
|
183
|
+
names.append(m.group(1).lower())
|
|
184
|
+
return names
|
|
185
|
+
|
|
186
|
+
def region(self) -> str:
|
|
187
|
+
return self.require("catalog", "region")
|
|
188
|
+
|
|
189
|
+
def cdn(self, region: str) -> str:
|
|
190
|
+
"""CDN base of a region, without a trailing slash."""
|
|
191
|
+
return self.require(f"servers.{region}", "cdn").rstrip("/")
|
|
192
|
+
|
|
193
|
+
def master(self, region: str | None) -> tuple[str, str]:
|
|
194
|
+
"""The setting that names the decoded master data of `region`: the --master flag, else
|
|
195
|
+
`[servers.<region>] master`, else `[paths] master` -> (section, key)."""
|
|
196
|
+
if region and self.origin("paths", "master") != "flag" and self.has(f"servers.{region}", "master"):
|
|
197
|
+
return f"servers.{region}", "master"
|
|
198
|
+
return "paths", "master"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------- the process's settings
|
|
202
|
+
_active: Config | None = None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def use(cfg: Config) -> Config:
|
|
206
|
+
"""Make `cfg` the settings of this process (external tools; worker processes call it with the parent's)."""
|
|
207
|
+
global _active
|
|
208
|
+
_active = cfg
|
|
209
|
+
return cfg
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def active() -> Config | None:
|
|
213
|
+
return _active
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def usable_cpus() -> int:
|
|
217
|
+
"""The CPUs this process may run on, which sizes the default worker counts: its affinity mask (taskset, a
|
|
218
|
+
container's cpuset), not the machine's CPU count. os.process_cpu_count (Python 3.13), else the affinity mask where
|
|
219
|
+
the platform reports one, else os.cpu_count; at least 1."""
|
|
220
|
+
count = getattr(os, "process_cpu_count", None)
|
|
221
|
+
if count is not None:
|
|
222
|
+
n = count()
|
|
223
|
+
elif hasattr(os, "sched_getaffinity"):
|
|
224
|
+
n = len(os.sched_getaffinity(0))
|
|
225
|
+
else:
|
|
226
|
+
n = os.cpu_count()
|
|
227
|
+
return max(1, n or 1)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def tool(name: str, exe: str) -> str:
|
|
231
|
+
"""An external program: `[paths] <name>` of the active settings, else `exe` on PATH."""
|
|
232
|
+
p = _active.path("paths", name) if _active is not None else None
|
|
233
|
+
if p is not None:
|
|
234
|
+
return str(p)
|
|
235
|
+
found = shutil.which(exe)
|
|
236
|
+
if not found:
|
|
237
|
+
raise ConfigError(f"{exe} not found on PATH: give its path as {describe('paths', name, '--' + name)}")
|
|
238
|
+
return found
|