cuju-render 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 (382) hide show
  1. cuju/render/__init__.py +759 -0
  2. cuju/render/__main__.py +1065 -0
  3. cuju/render/_export/__init__.py +3 -0
  4. cuju/render/_export/feed.py +542 -0
  5. cuju/render/_export/sizes.py +157 -0
  6. cuju/render/_export/video.py +1618 -0
  7. cuju/render/_export/video_jobs.py +408 -0
  8. cuju/render/_format/__init__.py +3 -0
  9. cuju/render/_format/annotations.py +235 -0
  10. cuju/render/_format/codec.py +307 -0
  11. cuju/render/_format/keep.py +121 -0
  12. cuju/render/_format/recording.py +860 -0
  13. cuju/render/_format/writer.py +54 -0
  14. cuju/render/_guide/__init__.py +3 -0
  15. cuju/render/_guide/doctor.py +523 -0
  16. cuju/render/_guide/help.py +62 -0
  17. cuju/render/_guide/tutorial.py +254 -0
  18. cuju/render/_http/__init__.py +3 -0
  19. cuju/render/_http/commands.py +57 -0
  20. cuju/render/_http/headers.py +103 -0
  21. cuju/render/_http/launch.py +500 -0
  22. cuju/render/_http/launchers.py +408 -0
  23. cuju/render/_http/pwa.py +128 -0
  24. cuju/render/_http/server.py +2105 -0
  25. cuju/render/_http/window.py +360 -0
  26. cuju/render/_moments/__init__.py +3 -0
  27. cuju/render/_moments/captions.py +284 -0
  28. cuju/render/_moments/events.py +1123 -0
  29. cuju/render/_moments/search.py +694 -0
  30. cuju/render/_packages/__init__.py +3 -0
  31. cuju/render/_packages/glsl.py +746 -0
  32. cuju/render/_packages/loudness.py +334 -0
  33. cuju/render/_packages/pack.py +1959 -0
  34. cuju/render/_packages/presentation.py +1376 -0
  35. cuju/render/_packages/vocabulary.py +609 -0
  36. cuju/render/_playing/__init__.py +3 -0
  37. cuju/render/_playing/achievements.py +463 -0
  38. cuju/render/_playing/bench.py +609 -0
  39. cuju/render/_playing/challenges.py +1813 -0
  40. cuju/render/_playing/demo.py +323 -0
  41. cuju/render/_playing/draw.py +211 -0
  42. cuju/render/_playing/errors.py +20 -0
  43. cuju/render/_playing/game.py +645 -0
  44. cuju/render/_playing/limits.py +339 -0
  45. cuju/render/_playing/play.py +926 -0
  46. cuju/render/_playing/policy_worker.py +250 -0
  47. cuju/render/_playing/strict.py +810 -0
  48. cuju/render/_registry/__init__.py +3 -0
  49. cuju/render/_registry/clips.py +394 -0
  50. cuju/render/_registry/joints.py +108 -0
  51. cuju/render/_registry/teams.py +2058 -0
  52. cuju/render/_store/__init__.py +3 -0
  53. cuju/render/_store/home.py +70 -0
  54. cuju/render/_store/library.py +604 -0
  55. cuju/render/_store/samples.py +163 -0
  56. cuju/render/_text/__init__.py +3 -0
  57. cuju/render/_text/languages.py +637 -0
  58. cuju/render/_text/messages.py +1396 -0
  59. cuju/render/_tournaments/__init__.py +3 -0
  60. cuju/render/_tournaments/results.py +547 -0
  61. cuju/render/_tournaments/tournament.py +2742 -0
  62. cuju/render/_tournaments/tournament_cli.py +587 -0
  63. cuju/render/_tournaments/tournament_runner.py +1975 -0
  64. cuju/render/_version.py +6 -0
  65. cuju/render/_watch/__init__.py +3 -0
  66. cuju/render/_watch/grid.py +265 -0
  67. cuju/render/_watch/live.py +664 -0
  68. cuju/render/_watch/notebook.py +79 -0
  69. cuju/render/_watch/show.py +171 -0
  70. cuju/render/samples/sample-11v11.cjr +0 -0
  71. cuju/render/samples/sample-5v5.cjr +0 -0
  72. cuju/render/samples/teams/starter/main.py +33 -0
  73. cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
  74. cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
  75. cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
  76. cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
  77. cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
  78. cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
  79. cuju/render/web/assets/adboard_polygon.webp +0 -0
  80. cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
  81. cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
  82. cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
  83. cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
  84. cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
  85. cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
  86. cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
  87. cuju/render/web/assets/ball.glb +0 -0
  88. cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
  89. cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
  90. cuju/render/web/assets/challenges/corner.mp4 +0 -0
  91. cuju/render/web/assets/challenges/corner.webp +0 -0
  92. cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
  93. cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
  94. cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
  95. cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
  96. cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
  97. cuju/render/web/assets/challenges/empty_goal.webp +0 -0
  98. cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
  99. cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
  100. cuju/render/web/assets/challenges/manifest.json +375 -0
  101. cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
  102. cuju/render/web/assets/challenges/one_on_one.webp +0 -0
  103. cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
  104. cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
  105. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
  106. cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
  107. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
  108. cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
  109. cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
  110. cuju/render/web/assets/challenges/run_to_score.webp +0 -0
  111. cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
  112. cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
  113. cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
  114. cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
  115. cuju/render/web/assets/crowd.webp +0 -0
  116. cuju/render/web/assets/crowd_normal.webp +0 -0
  117. cuju/render/web/assets/cuju-logo-384.png +0 -0
  118. cuju/render/web/assets/cuju-logo.png +0 -0
  119. cuju/render/web/assets/cuju-logo.svg +46 -0
  120. cuju/render/web/assets/cuju-mark-light.svg +33 -0
  121. cuju/render/web/assets/cuju-mark.svg +33 -0
  122. cuju/render/web/assets/goal.glb +0 -0
  123. cuju/render/web/assets/grass.webp +0 -0
  124. cuju/render/web/assets/hair_black.png +0 -0
  125. cuju/render/web/assets/hair_blonde.png +0 -0
  126. cuju/render/web/assets/hair_brown.png +0 -0
  127. cuju/render/web/assets/hair_darkblonde.png +0 -0
  128. cuju/render/web/assets/hair_long01.glb +0 -0
  129. cuju/render/web/assets/hair_long02.glb +0 -0
  130. cuju/render/web/assets/hair_medium01.glb +0 -0
  131. cuju/render/web/assets/hair_red.png +0 -0
  132. cuju/render/web/assets/hair_short01.glb +0 -0
  133. cuju/render/web/assets/hair_short02.glb +0 -0
  134. cuju/render/web/assets/icons/cuju-192.png +0 -0
  135. cuju/render/web/assets/icons/cuju-512.png +0 -0
  136. cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
  137. cuju/render/web/assets/kit_goalkeeper.webp +0 -0
  138. cuju/render/web/assets/kit_mask.png +0 -0
  139. cuju/render/web/assets/kit_normal.webp +0 -0
  140. cuju/render/web/assets/manifest.json +601 -0
  141. cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
  142. cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
  143. cuju/render/web/assets/models/kaykit-knight.glb +0 -0
  144. cuju/render/web/assets/models/kaykit-knight.json +23 -0
  145. cuju/render/web/assets/models/kaykit-mage.glb +0 -0
  146. cuju/render/web/assets/models/kaykit-mage.json +23 -0
  147. cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
  148. cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
  149. cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
  150. cuju/render/web/assets/models/kaykit-ranger.json +23 -0
  151. cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
  152. cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
  153. cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
  154. cuju/render/web/assets/models/kaykit-rogue.json +23 -0
  155. cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
  156. cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
  157. cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
  158. cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
  159. cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
  160. cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
  161. cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
  162. cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
  163. cuju/render/web/assets/pitch.webp +0 -0
  164. cuju/render/web/assets/pitch_normal.webp +0 -0
  165. cuju/render/web/assets/player.glb +0 -0
  166. cuju/render/web/assets/shoe.webp +0 -0
  167. cuju/render/web/assets/shoe_sole.png +0 -0
  168. cuju/render/web/assets/skin_01.png +0 -0
  169. cuju/render/web/assets/skin_02.png +0 -0
  170. cuju/render/web/assets/skin_03.png +0 -0
  171. cuju/render/web/assets/skin_04.png +0 -0
  172. cuju/render/web/assets/sounds/applause.ogg +0 -0
  173. cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
  174. cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
  175. cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
  176. cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
  177. cuju/render/web/assets/sounds/city-horn.ogg +0 -0
  178. cuju/render/web/assets/sounds/city-loop.ogg +0 -0
  179. cuju/render/web/assets/sounds/city-siren.ogg +0 -0
  180. cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
  181. cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
  182. cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
  183. cuju/render/web/assets/sounds/gulls.ogg +0 -0
  184. cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
  185. cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
  186. cuju/render/web/assets/sounds/sounds.json +46 -0
  187. cuju/render/web/assets/sounds/space-beep.ogg +0 -0
  188. cuju/render/web/assets/sounds/space-hum.ogg +0 -0
  189. cuju/render/web/assets/sounds/space-radio.ogg +0 -0
  190. cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
  191. cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
  192. cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
  193. cuju/render/web/easy.css +357 -0
  194. cuju/render/web/export.html +23 -0
  195. cuju/render/web/feed.css +262 -0
  196. cuju/render/web/feed.html +28 -0
  197. cuju/render/web/flat.css +120 -0
  198. cuju/render/web/home.css +837 -0
  199. cuju/render/web/index.html +373 -0
  200. cuju/render/web/js/boot.js +50 -0
  201. cuju/render/web/js/broadcast/audio.js +2141 -0
  202. cuju/render/web/js/broadcast/celebrate.js +1238 -0
  203. cuju/render/web/js/broadcast/decisions.js +609 -0
  204. cuju/render/web/js/broadcast/hud.js +352 -0
  205. cuju/render/web/js/broadcast/show.js +3614 -0
  206. cuju/render/web/js/broadcast/transition.js +2231 -0
  207. cuju/render/web/js/broadcast/voices.js +153 -0
  208. cuju/render/web/js/capture.js +101 -0
  209. cuju/render/web/js/core/accessibility.js +90 -0
  210. cuju/render/web/js/core/api.js +429 -0
  211. cuju/render/web/js/core/comfort.js +135 -0
  212. cuju/render/web/js/core/i18n.js +705 -0
  213. cuju/render/web/js/core/icons.js +167 -0
  214. cuju/render/web/js/core/input-settings.js +156 -0
  215. cuju/render/web/js/core/keys.js +277 -0
  216. cuju/render/web/js/core/language-ui.js +125 -0
  217. cuju/render/web/js/core/menu.js +853 -0
  218. cuju/render/web/js/core/names.js +113 -0
  219. cuju/render/web/js/core/page-structure.js +110 -0
  220. cuju/render/web/js/core/scenarios.js +109 -0
  221. cuju/render/web/js/core/spoilers.js +236 -0
  222. cuju/render/web/js/core/typeahead.js +38 -0
  223. cuju/render/web/js/export/dom-picture.js +330 -0
  224. cuju/render/web/js/export/export-host.js +97 -0
  225. cuju/render/web/js/export/feed-dialog.js +191 -0
  226. cuju/render/web/js/export/feed-params.js +266 -0
  227. cuju/render/web/js/export/feed.js +363 -0
  228. cuju/render/web/js/export/mp4-mux.js +295 -0
  229. cuju/render/web/js/export/photo-core.js +313 -0
  230. cuju/render/web/js/export/photo.js +1314 -0
  231. cuju/render/web/js/export/safe-frame.js +59 -0
  232. cuju/render/web/js/export/size-fields.js +103 -0
  233. cuju/render/web/js/export/sizes.js +178 -0
  234. cuju/render/web/js/export/video-boot.js +55 -0
  235. cuju/render/web/js/export/video-browser.js +325 -0
  236. cuju/render/web/js/export/video-export.js +417 -0
  237. cuju/render/web/js/export/video-plan.js +207 -0
  238. cuju/render/web/js/export/video-range.js +185 -0
  239. cuju/render/web/js/export/webm-mux.js +198 -0
  240. cuju/render/web/js/feed-boot.js +151 -0
  241. cuju/render/web/js/feed-frame.js +101 -0
  242. cuju/render/web/js/help/doctor.js +488 -0
  243. cuju/render/web/js/help/firstrun.js +379 -0
  244. cuju/render/web/js/help/help-center.js +438 -0
  245. cuju/render/web/js/help/install-offer.js +164 -0
  246. cuju/render/web/js/help/install-state.js +104 -0
  247. cuju/render/web/js/help/lessons-core.js +207 -0
  248. cuju/render/web/js/help/lessons.js +463 -0
  249. cuju/render/web/js/help/palette.js +584 -0
  250. cuju/render/web/js/help/pwa.js +257 -0
  251. cuju/render/web/js/help/tour-core.js +241 -0
  252. cuju/render/web/js/help/tour.js +461 -0
  253. cuju/render/web/js/main.js +2513 -0
  254. cuju/render/web/js/pages/draw.js +188 -0
  255. cuju/render/web/js/pages/fieldpage.js +530 -0
  256. cuju/render/web/js/pages/home.js +521 -0
  257. cuju/render/web/js/pages/matches.js +779 -0
  258. cuju/render/web/js/pages/moments-ui.js +359 -0
  259. cuju/render/web/js/pages/queue-filters.js +63 -0
  260. cuju/render/web/js/pages/recording-groups.js +80 -0
  261. cuju/render/web/js/pages/recordings.js +1069 -0
  262. cuju/render/web/js/play/bench.js +347 -0
  263. cuju/render/web/js/play/challenges.js +1302 -0
  264. cuju/render/web/js/play/medals.js +127 -0
  265. cuju/render/web/js/play/play.js +1953 -0
  266. cuju/render/web/js/play/teasers.js +196 -0
  267. cuju/render/web/js/play/touch.js +350 -0
  268. cuju/render/web/js/play/tournament-layout.js +487 -0
  269. cuju/render/web/js/play/tournament-results.js +441 -0
  270. cuju/render/web/js/play/tournament-strict.js +560 -0
  271. cuju/render/web/js/play/tournament.js +2585 -0
  272. cuju/render/web/js/players/animate.js +972 -0
  273. cuju/render/web/js/players/clips.js +224 -0
  274. cuju/render/web/js/players/kaykit.js +162 -0
  275. cuju/render/web/js/players/kits.js +676 -0
  276. cuju/render/web/js/players/number-patch.js +82 -0
  277. cuju/render/web/js/players/players.js +2694 -0
  278. cuju/render/web/js/players/recolor.js +893 -0
  279. cuju/render/web/js/players/retarget.js +325 -0
  280. cuju/render/web/js/players/rig-numbers.js +174 -0
  281. cuju/render/web/js/presentation/animator.js +231 -0
  282. cuju/render/web/js/presentation/assign.js +85 -0
  283. cuju/render/web/js/presentation/behaviors.js +440 -0
  284. cuju/render/web/js/presentation/budget.js +209 -0
  285. cuju/render/web/js/presentation/catalog.js +1674 -0
  286. cuju/render/web/js/presentation/clocks.js +72 -0
  287. cuju/render/web/js/presentation/commands.js +301 -0
  288. cuju/render/web/js/presentation/contract.js +240 -0
  289. cuju/render/web/js/presentation/effects.js +904 -0
  290. cuju/render/web/js/presentation/flash.js +605 -0
  291. cuju/render/web/js/presentation/freeze-worklet.js +22 -0
  292. cuju/render/web/js/presentation/glsl.js +464 -0
  293. cuju/render/web/js/presentation/host.js +323 -0
  294. cuju/render/web/js/presentation/index.js +2066 -0
  295. cuju/render/web/js/presentation/loudness-worklet.js +235 -0
  296. cuju/render/web/js/presentation/loudness.js +740 -0
  297. cuju/render/web/js/presentation/moments.js +536 -0
  298. cuju/render/web/js/presentation/post.js +327 -0
  299. cuju/render/web/js/presentation/preview.js +180 -0
  300. cuju/render/web/js/presentation/protocol.js +174 -0
  301. cuju/render/web/js/presentation/reach.js +158 -0
  302. cuju/render/web/js/presentation/rigging.js +130 -0
  303. cuju/render/web/js/presentation/sandbox.js +157 -0
  304. cuju/render/web/js/presentation/shaders.js +235 -0
  305. cuju/render/web/js/presentation/stage.js +402 -0
  306. cuju/render/web/js/presentation/teamaudio.js +722 -0
  307. cuju/render/web/js/presentation/tracks.js +197 -0
  308. cuju/render/web/js/presentation/values.js +81 -0
  309. cuju/render/web/js/recording/annotation-data.js +203 -0
  310. cuju/render/web/js/recording/captions.js +491 -0
  311. cuju/render/web/js/recording/codec.js +185 -0
  312. cuju/render/web/js/recording/decode-worker.js +70 -0
  313. cuju/render/web/js/recording/decode.js +94 -0
  314. cuju/render/web/js/recording/episodes.js +46 -0
  315. cuju/render/web/js/recording/format.js +461 -0
  316. cuju/render/web/js/recording/live.js +348 -0
  317. cuju/render/web/js/recording/moments-query.js +59 -0
  318. cuju/render/web/js/recording/moments.js +149 -0
  319. cuju/render/web/js/recording/pose.js +86 -0
  320. cuju/render/web/js/recording/timeline.js +275 -0
  321. cuju/render/web/js/teams/art.js +50 -0
  322. cuju/render/web/js/teams/clip-editor.js +293 -0
  323. cuju/render/web/js/teams/editor.js +1979 -0
  324. cuju/render/web/js/teams/library.js +344 -0
  325. cuju/render/web/js/teams/painter.js +633 -0
  326. cuju/render/web/js/teams/surprise-ui.js +195 -0
  327. cuju/render/web/js/teams/surprise.js +497 -0
  328. cuju/render/web/js/teams/team-check.js +127 -0
  329. cuju/render/web/js/watch/annotations.js +270 -0
  330. cuju/render/web/js/watch/flat.js +366 -0
  331. cuju/render/web/js/watch/grid.js +436 -0
  332. cuju/render/web/js/watch/pitch2d.js +289 -0
  333. cuju/render/web/js/watch/player-tools.js +192 -0
  334. cuju/render/web/js/watch/presets.js +201 -0
  335. cuju/render/web/js/world/arenas.js +997 -0
  336. cuju/render/web/js/world/ball.js +204 -0
  337. cuju/render/web/js/world/cameras.js +982 -0
  338. cuju/render/web/js/world/crowd.js +376 -0
  339. cuju/render/web/js/world/field.js +227 -0
  340. cuju/render/web/js/world/framing.js +145 -0
  341. cuju/render/web/js/world/occlusion.js +48 -0
  342. cuju/render/web/js/world/pitch.js +703 -0
  343. cuju/render/web/js/world/scene.js +395 -0
  344. cuju/render/web/js/world/sky.js +325 -0
  345. cuju/render/web/js/world/stadium.js +1262 -0
  346. cuju/render/web/js/world/vendor-sky.js +367 -0
  347. cuju/render/web/js/world/weather.js +264 -0
  348. cuju/render/web/languages/en.json +4750 -0
  349. cuju/render/web/lessons.json +137 -0
  350. cuju/render/web/manifest.webmanifest +23 -0
  351. cuju/render/web/style.css +8399 -0
  352. cuju/render/web/sw.js +235 -0
  353. cuju/render/web/vendor/fabric/LICENSE +16 -0
  354. cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
  355. cuju/render/web/vendor/lucide/LICENSE +43 -0
  356. cuju/render/web/vendor/lucide/icons.js +141 -0
  357. cuju/render/web/vendor/three/LICENSE +21 -0
  358. cuju/render/web/vendor/three/build/three.core.js +60586 -0
  359. cuju/render/web/vendor/three/build/three.module.js +19719 -0
  360. cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
  361. cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
  362. cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
  363. cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
  364. cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
  365. cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
  366. cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
  367. cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
  368. cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
  369. cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
  370. cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
  371. cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
  372. cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
  373. cuju_render-0.1.0.dist-info/METADATA +116 -0
  374. cuju_render-0.1.0.dist-info/RECORD +382 -0
  375. cuju_render-0.1.0.dist-info/WHEEL +4 -0
  376. cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
  377. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
  378. cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
  379. cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
  380. cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
  381. cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
  382. cuju_render-0.1.0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,54 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The writer of recorders, one thread that codes and writes recordings while training steps on.
5
+
6
+ A recorder hands the frames of an ended episode over and returns at once, and the writer codes and
7
+ writes them in the order they came. The queue holds a few recordings, so a recorder that saves
8
+ faster than the writer keeps up waits for room. The process waits for the queue when it exits.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import atexit
14
+ import queue
15
+ import threading
16
+ from collections.abc import Callable
17
+
18
+ # The recordings that wait for the writer, beyond which a save waits for room.
19
+ _ROOM = 4
20
+
21
+
22
+ class Writer:
23
+ """Runs jobs one after another on a thread of its own, started at the first job."""
24
+
25
+ def __init__(self, room: int = _ROOM) -> None:
26
+ self._queue: queue.Queue[Callable[[], None]] = queue.Queue(room)
27
+ self._lock = threading.Lock()
28
+ self._thread: threading.Thread | None = None
29
+
30
+ def submit(self, job: Callable[[], None]) -> None:
31
+ """Queues a job, waiting for room when the queue is full. A job must not raise."""
32
+ with self._lock:
33
+ if self._thread is None or not self._thread.is_alive():
34
+ self._thread = threading.Thread(
35
+ target=self._run, name='cuju-render-writer', daemon=True
36
+ )
37
+ self._thread.start()
38
+ self._queue.put(job)
39
+
40
+ def join(self) -> None:
41
+ """Waits until every queued job has run."""
42
+ self._queue.join()
43
+
44
+ def _run(self) -> None:
45
+ while True:
46
+ job = self._queue.get()
47
+ try:
48
+ job()
49
+ finally:
50
+ self._queue.task_done()
51
+
52
+
53
+ WRITER = Writer()
54
+ atexit.register(WRITER.join)
@@ -0,0 +1,3 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """The Help page, the tutorial for agents and the diagnostic report."""
@@ -0,0 +1,523 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The diagnostic report of the renderer, which the Help page and `cuju render doctor` make.
5
+
6
+ The report gathers the versions of cuju-render, cuju, Python, PyTorch and NumPy, the system, the
7
+ checks of the renderer's home folder, the files and bytes of each of its folders, the counts of the
8
+ library and the registry, the server of the folder and the recent warnings and errors of the
9
+ server. The page adds what the browser can do. A report names no path beyond the home folder, and
10
+ writes the user's home directory in it and in every message as `~`, the renderer's home folder
11
+ outside it as `$CUJU_RENDER_HOME` and the user's name as `<user>`. It holds no key and no variable
12
+ of the environment, and only the messages of errors may name a recording or a team.
13
+
14
+ The server keeps its recent warnings and errors in memory, the records of the logger `cuju` from
15
+ `install_capture` on and every request that failed inside the server, which `note_error` adds.
16
+ `GET /api/help/doctor` returns `server_report`, and the command asks a running server for it.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import collections
23
+ import getpass
24
+ import http.client
25
+ import importlib.metadata
26
+ import json
27
+ import logging
28
+ import os
29
+ import platform
30
+ import re
31
+ import shutil
32
+ import sys
33
+ import threading
34
+ import time
35
+ from pathlib import Path
36
+ from typing import Any
37
+
38
+ from cuju import __version__
39
+
40
+ from .. import _version
41
+ from .._http import launch as _launch
42
+ from .._store import home as _home
43
+ from .._text import messages as _messages
44
+ from .._text.messages import counted
45
+
46
+ # The recent warnings and errors that the server keeps.
47
+ _KEPT = 50
48
+ _RECENT: collections.deque[dict[str, Any]] = collections.deque(maxlen=_KEPT)
49
+ _RECENT_LOCK = threading.Lock()
50
+ # A home folder with less free space than this fails the check of the disk.
51
+ LOW_DISK_BYTES = 500 * 1024 * 1024
52
+ # The lines of server.log that the command reads, from its end.
53
+ _LOG_LINES = 400
54
+ _LOG_WORDS = ('error', 'traceback', 'exception', 'failed')
55
+
56
+
57
+ # Windows reads a path or a name whatever its case, so the scrub matches them so there.
58
+ _CASELESS = os.name == 'nt'
59
+
60
+
61
+ def _spellings(path: str) -> set[str]:
62
+ """Returns a path as written, resolved and in either kind of slash, or none for a root."""
63
+ if not path or path in ('~', '.') or Path(path).parent == Path(path):
64
+ return set()
65
+ found = set()
66
+ for written in (path, str(Path(path).resolve())):
67
+ found |= {written, written.replace('\\', '/'), written.replace('/', '\\')}
68
+ return found
69
+
70
+
71
+ def _user_names() -> set[str]:
72
+ """Returns the names of the user that a message may hold, each of two characters or more."""
73
+ names = {os.environ.get('USERNAME', ''), os.environ.get('USER', ''), Path.home().name}
74
+ try:
75
+ names.add(getpass.getuser())
76
+ except Exception:
77
+ # Some systems name no user for the process, and the other names stand.
78
+ pass
79
+ return {name for name in names if len(name.strip()) >= 2}
80
+
81
+
82
+ def scrub(text: Any) -> str:
83
+ """Returns `text` with the private paths and names of the user written as placeholders.
84
+
85
+ The user's home directory becomes `~`, the renderer's home folder, when it lies outside that,
86
+ `$CUJU_RENDER_HOME`, and the user's name, as a whole word, `<user>`. A path matches in either
87
+ kind of slash, and on Windows a path or a name matches whatever its case.
88
+ """
89
+ text = str(text)
90
+ flags = re.IGNORECASE if _CASELESS else 0
91
+ users = [path for path in (os.path.expanduser('~'), str(Path.home())) if _spellings(path)]
92
+ renderer = _home.home()
93
+ # A renderer's home inside the user's reads as a path under `~`, which names nothing private.
94
+ inside = any(renderer.resolve().is_relative_to(Path(path).resolve()) for path in users)
95
+ replacements = [(spelling, '~') for path in users for spelling in _spellings(path)]
96
+ if not inside:
97
+ token = f'${_home.HOME_VARIABLE}'
98
+ replacements += [(spelling, token) for spelling in _spellings(str(renderer))]
99
+ for found, written in sorted(replacements, key=lambda pair: len(pair[0]), reverse=True):
100
+ text = re.sub(re.escape(found), lambda _, written=written: written, text, flags=flags)
101
+ for name in sorted(_user_names(), key=len, reverse=True):
102
+ text = re.sub(rf'(?<!\w){re.escape(name)}(?!\w)', '<user>', text, flags=flags)
103
+ return text
104
+
105
+
106
+ class _Capture(logging.Handler):
107
+ """Keeps the recent warnings and errors of the logger `cuju`.
108
+
109
+ Python prints a warning to the standard error stream only while no handler would take it, so
110
+ the capture prints it there itself in that case, and the server's console shows what it showed
111
+ before.
112
+ """
113
+
114
+ def emit(self, record: logging.LogRecord) -> None:
115
+ try:
116
+ message = record.getMessage()
117
+ except Exception:
118
+ message = str(record.msg)
119
+ _keep(record.levelname, message)
120
+ if not _other_handlers(self) and logging.lastResort is not None:
121
+ if record.levelno >= logging.lastResort.level:
122
+ logging.lastResort.handle(record)
123
+
124
+
125
+ def _other_handlers(own: logging.Handler) -> bool:
126
+ logger: logging.Logger | None = logging.getLogger('cuju')
127
+ while logger is not None:
128
+ if any(handler is not own for handler in logger.handlers):
129
+ return True
130
+ if not logger.propagate:
131
+ break
132
+ logger = logger.parent
133
+ return False
134
+
135
+
136
+ def _keep(level: str, message: str) -> None:
137
+ with _RECENT_LOCK:
138
+ _RECENT.append(
139
+ {'at': time.strftime('%Y-%m-%dT%H:%M:%S'), 'level': level, 'message': scrub(message)}
140
+ )
141
+
142
+
143
+ def install_capture() -> None:
144
+ """Starts keeping the recent warnings and errors of the logger `cuju`, once per process."""
145
+ logger = logging.getLogger('cuju')
146
+ if not any(isinstance(handler, _Capture) for handler in logger.handlers):
147
+ handler = _Capture(logging.WARNING)
148
+ logger.addHandler(handler)
149
+
150
+
151
+ def note_error(text: str) -> None:
152
+ """Keeps an error of the server that no logger saw, such as a request that failed inside."""
153
+ _keep('ERROR', text)
154
+
155
+
156
+ def recent() -> list[dict[str, Any]]:
157
+ """Returns the recent warnings and errors, the oldest first."""
158
+ with _RECENT_LOCK:
159
+ return list(_RECENT)
160
+
161
+
162
+ def _version_of(package: str) -> str | None:
163
+ try:
164
+ return importlib.metadata.version(package)
165
+ except importlib.metadata.PackageNotFoundError:
166
+ return None
167
+
168
+
169
+ def _versions() -> dict[str, Any]:
170
+ return {
171
+ 'cuju-render': _version.__version__,
172
+ 'cuju': __version__,
173
+ 'python': platform.python_version(),
174
+ 'implementation': platform.python_implementation(),
175
+ 'system': platform.system(),
176
+ 'release': platform.release(),
177
+ 'machine': platform.machine(),
178
+ 'torch': _version_of('torch'),
179
+ 'numpy': _version_of('numpy'),
180
+ }
181
+
182
+
183
+ def _home_facts() -> dict[str, Any]:
184
+ folder = _home.home()
185
+ # The ids of the messages of the problems, whose English texts the report holds.
186
+ found = []
187
+ existing = folder
188
+ while not existing.exists() and existing != existing.parent:
189
+ existing = existing.parent
190
+ if folder.exists() and not folder.is_dir():
191
+ found.append('server.doctor.home.file')
192
+ elif not folder.exists():
193
+ if existing.exists() and not existing.is_dir():
194
+ found.append('server.doctor.home.under')
195
+ else:
196
+ found.append('server.doctor.home.missing')
197
+ elif not os.access(folder, os.W_OK | os.X_OK):
198
+ found.append('server.doctor.home.readonly')
199
+ problems = [_messages.text(message_id) for message_id in found]
200
+ free = None
201
+ try:
202
+ free = shutil.disk_usage(existing if existing.is_dir() else existing.parent).free
203
+ except OSError:
204
+ free = None
205
+ return {
206
+ 'path': scrub(folder),
207
+ 'moved': bool(os.environ.get(_home.HOME_VARIABLE)),
208
+ 'exists': folder.is_dir(),
209
+ 'writable': folder.is_dir() and os.access(folder, os.W_OK | os.X_OK),
210
+ 'free_bytes': free,
211
+ 'problems': problems,
212
+ 'problem_ids': found,
213
+ }
214
+
215
+
216
+ def _folders() -> dict[str, dict[str, int]]:
217
+ folder = _home.home()
218
+ sizes: dict[str, dict[str, int]] = {}
219
+ if not folder.is_dir():
220
+ return sizes
221
+ for child in sorted(folder.iterdir()):
222
+ if not child.is_dir():
223
+ continue
224
+ files = 0
225
+ size = 0
226
+ for path in child.rglob('*'):
227
+ try:
228
+ if path.is_file():
229
+ files += 1
230
+ size += path.stat().st_size
231
+ except OSError:
232
+ continue
233
+ sizes[child.name] = {'files': files, 'bytes': size}
234
+ loose = [path for path in folder.iterdir() if path.is_file()]
235
+ sizes['.'] = {'files': len(loose), 'bytes': sum(path.stat().st_size for path in loose)}
236
+ return sizes
237
+
238
+
239
+ def _library_counts() -> dict[str, Any]:
240
+ from .._http import server as _server
241
+ from .._registry import teams as _teams
242
+ from .._store import library as _library
243
+
244
+ counts: dict[str, Any] = {}
245
+ try:
246
+ counts['recordings'] = len(_library.matches())
247
+ counts['trash'] = len(_library.matches(trash=True))
248
+ except OSError as error:
249
+ counts['recordings_error'] = scrub(error)
250
+ try:
251
+ counts['teams'] = sum(1 for entry in _teams.teams() if entry.get('kind') != 'builtin')
252
+ except OSError as error:
253
+ counts['teams_error'] = scrub(error)
254
+ tournaments = _home.home() / 'tournaments'
255
+ counts['tournaments'] = len(list(tournaments.glob('*.json'))) if tournaments.is_dir() else 0
256
+ boards = 0
257
+ challenges = _home.home() / 'challenges'
258
+ for path in challenges.glob('*.json') if challenges.is_dir() else []:
259
+ try:
260
+ document = json.loads(path.read_text(encoding='utf-8'))
261
+ except (OSError, ValueError):
262
+ continue
263
+ if isinstance(document, dict) and document.get('entries'):
264
+ boards += 1
265
+ counts['challenge_boards'] = boards
266
+ try:
267
+ counts['live_sources'] = len(_server._sources())
268
+ except OSError:
269
+ counts['live_sources'] = 0
270
+ return counts
271
+
272
+
273
+ def _server_facts() -> dict[str, Any]:
274
+ path = _home.home() / _launch.SERVER_FILE
275
+ # The file is looked at first, since asking for the running server removes a stale one.
276
+ found = path.is_file()
277
+ server = _launch.running()
278
+ if server is not None:
279
+ return {'file': True, 'running': True, 'port': server.port, 'stale': False}
280
+ return {'file': found, 'running': False, 'port': None, 'stale': found}
281
+
282
+
283
+ def _scrubbed(label: dict[str, Any]) -> dict[str, Any]:
284
+ """Returns a label of `_messages.label` with its text and its values scrubbed."""
285
+ values = {
286
+ key: _scrubbed(value) if isinstance(value, dict) else scrub(value)
287
+ for key, value in (label.get('values') or {}).items()
288
+ }
289
+ return {**label, 'text': scrub(label['text']), 'values': values}
290
+
291
+
292
+ def _check(key: str, ok: bool, detail: dict[str, Any] | str, **more: Any) -> dict[str, Any]:
293
+ """Returns a check of the report, its name and detail in English and, beside them, by id.
294
+
295
+ The name is the message `server.doctor.check.<key>`, and the detail a label of
296
+ `_messages.label` or, for an error of another kind, its text, so the page shows both in its
297
+ own language and the text of the command reads the English.
298
+ """
299
+ return {
300
+ **_messages.named('name', _messages.label(f'server.doctor.check.{key}')),
301
+ 'ok': ok,
302
+ **more,
303
+ **_messages.named('detail', detail),
304
+ }
305
+
306
+
307
+ def _checks(report: dict[str, Any]) -> list[dict[str, Any]]:
308
+ from .._store import library as _library
309
+
310
+ label = _messages.label
311
+ home = report['home']
312
+ # The checks of the home folder find one problem at most.
313
+ problems = home.get('problem_ids') or []
314
+ place = (
315
+ label(problems[0]) if problems else label('server.doctor.home.exists', path=home['path'])
316
+ )
317
+ checks = [
318
+ _check('home_folder', home['exists'] and not home['problems'], place),
319
+ _check(
320
+ 'writing',
321
+ bool(home['writable']),
322
+ label('server.doctor.writing.ok' if home['writable'] else 'server.doctor.writing.fail'),
323
+ ),
324
+ ]
325
+ free = home['free_bytes']
326
+ if free is None:
327
+ space = label('server.doctor.disk.unknown')
328
+ else:
329
+ low = 'free' if free >= LOW_DISK_BYTES else 'low'
330
+ space = label(f'server.doctor.disk.{low}', size=f'{free / 1024**3:.1f}')
331
+ checks.append(_check('disk_space', free is None or free >= LOW_DISK_BYTES, space))
332
+ shipped = _library.samples()
333
+ checks.append(
334
+ _check('samples', len(shipped) >= 2, label('server.doctor.samples', count=len(shipped)))
335
+ )
336
+ page = Path(__file__).parents[1] / 'web' / 'index.html'
337
+ shown = 'ok' if page.is_file() else 'missing'
338
+ checks.append(_check('page_files', page.is_file(), label(f'server.doctor.page.{shown}')))
339
+ server = report['server']
340
+ state = 'stale' if server['stale'] else 'running' if server['running'] else 'none'
341
+ checks.append(
342
+ _check('server_file', not server['stale'], label(f'server.doctor.server_file.{state}'))
343
+ )
344
+ try:
345
+ from .._export import video as _video
346
+ from .._export import video_jobs as _video_jobs
347
+
348
+ video = _video_jobs.availability()
349
+ labels = video.get('missing_labels') or []
350
+ if video['available']:
351
+ detail = label('server.doctor.video.found', browser=_browser_name(_video.find_chrome()))
352
+ elif labels and all(found.get('id') for found in labels):
353
+ detail = _scrubbed(_video.missing_label(labels))
354
+ else:
355
+ detail = scrub(' '.join(video['missing']))
356
+ # Video export is optional, so a missing program warns and passes.
357
+ checks.append(_check('video_export', True, detail, warning=not video['available']))
358
+ except Exception as error:
359
+ checks.append(_check('video_export', True, scrub(error), warning=True))
360
+ return checks
361
+
362
+
363
+ def _browser_name(path: str | None) -> str:
364
+ """Returns the name of the browser at a path, such as Google Chrome, else its file name."""
365
+ stem = Path(path or '').stem.lower()
366
+ names = {
367
+ 'chrome': 'Google Chrome',
368
+ 'google chrome': 'Google Chrome',
369
+ 'google-chrome': 'Google Chrome',
370
+ 'google-chrome-stable': 'Google Chrome',
371
+ 'chromium': 'Chromium',
372
+ 'chromium-browser': 'Chromium',
373
+ 'msedge': 'Microsoft Edge',
374
+ 'microsoft edge': 'Microsoft Edge',
375
+ }
376
+ return names.get(stem, Path(path or '').name or 'Chrome')
377
+
378
+
379
+ def server_report() -> dict[str, Any]:
380
+ """Returns the server's side of the diagnostic report, a document of plain values."""
381
+ report: dict[str, Any] = {
382
+ 'kind': 'server',
383
+ 'created': time.strftime('%Y-%m-%dT%H:%M:%S'),
384
+ 'versions': _versions(),
385
+ 'home': _home_facts(),
386
+ 'folders': _folders(),
387
+ 'library': _library_counts(),
388
+ 'server': _server_facts(),
389
+ 'errors': recent(),
390
+ }
391
+ report['checks'] = _checks(report)
392
+ return report
393
+
394
+
395
+ def _log_errors() -> list[dict[str, Any]]:
396
+ """Returns the lines of server.log near its end that name an error."""
397
+ path = _home.home() / _launch.LOG_FILE
398
+ try:
399
+ lines = path.read_text(encoding='utf-8', errors='replace').splitlines()[-_LOG_LINES:]
400
+ except OSError:
401
+ return []
402
+ found = [line for line in lines if any(word in line.lower() for word in _LOG_WORDS)]
403
+ return [{'at': None, 'level': 'LOG', 'message': scrub(line)} for line in found[-20:]]
404
+
405
+
406
+ def _ask_server(port: int, timeout: float = 3.0) -> dict[str, Any] | None:
407
+ connection = http.client.HTTPConnection('127.0.0.1', port, timeout=timeout)
408
+ try:
409
+ connection.request('GET', '/api/help/doctor', headers={'Host': f'127.0.0.1:{port}'})
410
+ response = connection.getresponse()
411
+ body = response.read()
412
+ if response.status != 200:
413
+ return None
414
+ document = json.loads(body)
415
+ return document if isinstance(document, dict) else None
416
+ except (OSError, ValueError):
417
+ return None
418
+ finally:
419
+ connection.close()
420
+
421
+
422
+ def report() -> dict[str, Any]:
423
+ """Returns the report of the command, the local view with the running server's own.
424
+
425
+ The server's recent errors come from the server itself, and the lines of server.log that
426
+ name an error join them.
427
+ """
428
+ local = server_report()
429
+ local['kind'] = 'command'
430
+ local['errors'] = _log_errors()
431
+ if local['server']['running']:
432
+ answer = _ask_server(local['server']['port'])
433
+ local['server']['report'] = answer
434
+ if answer is not None:
435
+ local['errors'] = [*local['errors'], *answer.get('errors', [])]
436
+ versions = answer.get('versions', {})
437
+ if versions.get('cuju') != __version__:
438
+ detail = _messages.label(
439
+ 'server.doctor.server_version',
440
+ server=versions.get('cuju'),
441
+ command=__version__,
442
+ )
443
+ local['checks'].append(_check('server_version', False, detail))
444
+ return local
445
+
446
+
447
+ def _size(value: int | None) -> str:
448
+ if value is None:
449
+ return 'unknown'
450
+ for unit in ('bytes', 'KB', 'MB', 'GB'):
451
+ if value < 1024 or unit == 'GB':
452
+ return f'{value} bytes' if unit == 'bytes' else f'{value:.1f} {unit}'
453
+ value /= 1024
454
+ return str(value)
455
+
456
+
457
+ def text(document: dict[str, Any]) -> str:
458
+ """Returns a report as lines of text, the way it reads in an issue."""
459
+ lines = [f'cuju render diagnostic report, {document.get("created", "")}', '']
460
+ versions = document.get('versions', {})
461
+ lines.append('Versions')
462
+ for key in (
463
+ 'cuju-render',
464
+ 'cuju',
465
+ 'python',
466
+ 'implementation',
467
+ 'system',
468
+ 'release',
469
+ 'machine',
470
+ 'torch',
471
+ 'numpy',
472
+ ):
473
+ if key in versions:
474
+ lines.append(
475
+ f' {key}: {versions[key] if versions[key] is not None else "not installed"}'
476
+ )
477
+ home = document.get('home', {})
478
+ lines += ['', 'Home folder', f' path: {home.get("path")}']
479
+ lines.append(f' moved by {_home.HOME_VARIABLE}: {"yes" if home.get("moved") else "no"}')
480
+ lines.append(f' exists: {"yes" if home.get("exists") else "no"}')
481
+ lines.append(f' writable: {"yes" if home.get("writable") else "no"}')
482
+ lines.append(f' free space: {_size(home.get("free_bytes"))}')
483
+ lines += ['', 'Folders']
484
+ for name, entry in document.get('folders', {}).items():
485
+ lines.append(f' {name}: {counted(entry["files"], "file")}, {_size(entry["bytes"])}')
486
+ lines += ['', 'Library']
487
+ for name, value in document.get('library', {}).items():
488
+ lines.append(f' {name.replace("_", " ")}: {value}')
489
+ server = document.get('server', {})
490
+ lines += ['', 'Server']
491
+ if server.get('running'):
492
+ lines.append(f' runs on port {server.get("port")}')
493
+ else:
494
+ lines.append(' no server runs' + (', server.json is stale' if server.get('stale') else ''))
495
+ lines += ['', 'Checks']
496
+ for check in document.get('checks', []):
497
+ mark = 'FAIL' if not check['ok'] else 'warn' if check.get('warning') else 'ok'
498
+ lines.append(f' {mark} {check["name"]}, {check["detail"]}')
499
+ errors = document.get('errors', [])
500
+ lines += ['', 'Recent warnings and errors']
501
+ if not errors:
502
+ lines.append(' none')
503
+ for entry in errors:
504
+ when = f'{entry["at"]} ' if entry.get('at') else ''
505
+ lines.append(f' {when}{entry["level"]} {entry["message"]}')
506
+ return '\n'.join(lines)
507
+
508
+
509
+ def add_arguments(parser: argparse.ArgumentParser) -> None:
510
+ """Adds the options of `cuju render doctor`."""
511
+ parser.add_argument('--json', action='store_true', help='print the report as JSON')
512
+ parser.add_argument('--out', help='write the report to this file as well')
513
+
514
+
515
+ def run(options: argparse.Namespace) -> int:
516
+ """Runs `cuju render doctor`, and returns 0 when every check passes and 1 otherwise."""
517
+ document = report()
518
+ output = json.dumps(document, indent=2) if options.json else text(document)
519
+ print(output)
520
+ if options.out:
521
+ Path(options.out).write_text(output + '\n', encoding='utf-8')
522
+ print(f'wrote {options.out}', file=sys.stderr)
523
+ return 0 if all(check['ok'] for check in document['checks']) else 1
@@ -0,0 +1,62 @@
1
+ # Copyright 2026 The cuju Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """The routes of the Help page under `/api/help`, the samples, the tutorial, the diagnostic report
5
+ and the facts of the installable page.
6
+
7
+ - `GET /api/help/samples` lists the shipped samples with what the library holds of each, and
8
+ `POST /api/help/samples` adds or restores those its `names` list, by default every one.
9
+ - `GET /api/help/tutorial` returns every lesson of the tutorial with its check, and
10
+ `POST /api/help/tutorial` keeps the lessons a page reports in `done`, or clears them with
11
+ `reset`.
12
+ - `GET /api/help/doctor` returns the server's side of the diagnostic report.
13
+ - `GET /api/help/app` returns the version and the build of the page, which the installed page
14
+ compares with the files it keeps.
15
+
16
+ The server passes every request here through the same checks of host and origin as any other.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ from .._text import messages as _messages
24
+
25
+
26
+ def get(parts: list[str], query: dict[str, str]) -> tuple[Any, int]:
27
+ """Answers `GET /api/help/...` with a document and a status."""
28
+ if parts == ['samples']:
29
+ from .._store import samples as _samples
30
+
31
+ return _samples.listing(), 200
32
+ if parts[:1] == ['tutorial']:
33
+ from . import tutorial as _tutorial
34
+
35
+ return _tutorial.get(parts[1:], query)
36
+ if parts == ['doctor']:
37
+ from . import doctor as _doctor
38
+
39
+ return _doctor.server_report(), 200
40
+ if parts == ['app']:
41
+ from .._http import pwa as _pwa
42
+
43
+ return _pwa.app_info(), 200
44
+ return _messages.fields('server.no_resource'), 404
45
+
46
+
47
+ def post(parts: list[str], document: dict[str, Any]) -> tuple[Any, int]:
48
+ """Answers `POST /api/help/...` with a document and a status."""
49
+ if parts == ['samples']:
50
+ from .._store import samples as _samples
51
+
52
+ names = document.get('names')
53
+ if names is not None and (
54
+ not isinstance(names, list) or not all(isinstance(name, str) for name in names)
55
+ ):
56
+ return _messages.fields('server.help.sample_names', names=repr(names)), 400
57
+ return _samples.add(names), 200
58
+ if parts[:1] == ['tutorial']:
59
+ from . import tutorial as _tutorial
60
+
61
+ return _tutorial.post(parts[1:], document)
62
+ return _messages.fields('server.no_resource'), 404