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.
- cuju/render/__init__.py +759 -0
- cuju/render/__main__.py +1065 -0
- cuju/render/_export/__init__.py +3 -0
- cuju/render/_export/feed.py +542 -0
- cuju/render/_export/sizes.py +157 -0
- cuju/render/_export/video.py +1618 -0
- cuju/render/_export/video_jobs.py +408 -0
- cuju/render/_format/__init__.py +3 -0
- cuju/render/_format/annotations.py +235 -0
- cuju/render/_format/codec.py +307 -0
- cuju/render/_format/keep.py +121 -0
- cuju/render/_format/recording.py +860 -0
- cuju/render/_format/writer.py +54 -0
- cuju/render/_guide/__init__.py +3 -0
- cuju/render/_guide/doctor.py +523 -0
- cuju/render/_guide/help.py +62 -0
- cuju/render/_guide/tutorial.py +254 -0
- cuju/render/_http/__init__.py +3 -0
- cuju/render/_http/commands.py +57 -0
- cuju/render/_http/headers.py +103 -0
- cuju/render/_http/launch.py +500 -0
- cuju/render/_http/launchers.py +408 -0
- cuju/render/_http/pwa.py +128 -0
- cuju/render/_http/server.py +2105 -0
- cuju/render/_http/window.py +360 -0
- cuju/render/_moments/__init__.py +3 -0
- cuju/render/_moments/captions.py +284 -0
- cuju/render/_moments/events.py +1123 -0
- cuju/render/_moments/search.py +694 -0
- cuju/render/_packages/__init__.py +3 -0
- cuju/render/_packages/glsl.py +746 -0
- cuju/render/_packages/loudness.py +334 -0
- cuju/render/_packages/pack.py +1959 -0
- cuju/render/_packages/presentation.py +1376 -0
- cuju/render/_packages/vocabulary.py +609 -0
- cuju/render/_playing/__init__.py +3 -0
- cuju/render/_playing/achievements.py +463 -0
- cuju/render/_playing/bench.py +609 -0
- cuju/render/_playing/challenges.py +1813 -0
- cuju/render/_playing/demo.py +323 -0
- cuju/render/_playing/draw.py +211 -0
- cuju/render/_playing/errors.py +20 -0
- cuju/render/_playing/game.py +645 -0
- cuju/render/_playing/limits.py +339 -0
- cuju/render/_playing/play.py +926 -0
- cuju/render/_playing/policy_worker.py +250 -0
- cuju/render/_playing/strict.py +810 -0
- cuju/render/_registry/__init__.py +3 -0
- cuju/render/_registry/clips.py +394 -0
- cuju/render/_registry/joints.py +108 -0
- cuju/render/_registry/teams.py +2058 -0
- cuju/render/_store/__init__.py +3 -0
- cuju/render/_store/home.py +70 -0
- cuju/render/_store/library.py +604 -0
- cuju/render/_store/samples.py +163 -0
- cuju/render/_text/__init__.py +3 -0
- cuju/render/_text/languages.py +637 -0
- cuju/render/_text/messages.py +1396 -0
- cuju/render/_tournaments/__init__.py +3 -0
- cuju/render/_tournaments/results.py +547 -0
- cuju/render/_tournaments/tournament.py +2742 -0
- cuju/render/_tournaments/tournament_cli.py +587 -0
- cuju/render/_tournaments/tournament_runner.py +1975 -0
- cuju/render/_version.py +6 -0
- cuju/render/_watch/__init__.py +3 -0
- cuju/render/_watch/grid.py +265 -0
- cuju/render/_watch/live.py +664 -0
- cuju/render/_watch/notebook.py +79 -0
- cuju/render/_watch/show.py +171 -0
- cuju/render/samples/sample-11v11.cjr +0 -0
- cuju/render/samples/sample-5v5.cjr +0 -0
- cuju/render/samples/teams/starter/main.py +33 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_easy.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_hard.py +46 -0
- cuju/render/scenarios/benchmark_full_game_11_vs_11_medium.py +46 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_easy.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_hard.py +34 -0
- cuju/render/scenarios/benchmark_full_game_5_vs_5_medium.py +34 -0
- cuju/render/web/assets/adboard_polygon.webp +0 -0
- cuju/render/web/assets/adboard_your_ad_here.webp +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-combatmelee.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-general.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementadvanced.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-movementbasic.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-simulation.glb +0 -0
- cuju/render/web/assets/animations/kaykit-rig-medium-special.glb +0 -0
- cuju/render/web/assets/ball.glb +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/3_vs_1_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/corner.mp4 +0 -0
- cuju/render/web/assets/challenges/corner.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_easy.webp +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/counterattack_hard.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal.webp +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.mp4 +0 -0
- cuju/render/web/assets/challenges/empty_goal_close.webp +0 -0
- cuju/render/web/assets/challenges/manifest.json +375 -0
- cuju/render/web/assets/challenges/one_on_one.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one.webp +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.mp4 +0 -0
- cuju/render/web/assets/challenges/one_on_one_hard.webp +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_pass_and_shoot_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score.webp +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.mp4 +0 -0
- cuju/render/web/assets/challenges/run_to_score_with_keeper.webp +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.mp4 +0 -0
- cuju/render/web/assets/challenges/single_goal_versus_lazy.webp +0 -0
- cuju/render/web/assets/crowd.webp +0 -0
- cuju/render/web/assets/crowd_normal.webp +0 -0
- cuju/render/web/assets/cuju-logo-384.png +0 -0
- cuju/render/web/assets/cuju-logo.png +0 -0
- cuju/render/web/assets/cuju-logo.svg +46 -0
- cuju/render/web/assets/cuju-mark-light.svg +33 -0
- cuju/render/web/assets/cuju-mark.svg +33 -0
- cuju/render/web/assets/goal.glb +0 -0
- cuju/render/web/assets/grass.webp +0 -0
- cuju/render/web/assets/hair_black.png +0 -0
- cuju/render/web/assets/hair_blonde.png +0 -0
- cuju/render/web/assets/hair_brown.png +0 -0
- cuju/render/web/assets/hair_darkblonde.png +0 -0
- cuju/render/web/assets/hair_long01.glb +0 -0
- cuju/render/web/assets/hair_long02.glb +0 -0
- cuju/render/web/assets/hair_medium01.glb +0 -0
- cuju/render/web/assets/hair_red.png +0 -0
- cuju/render/web/assets/hair_short01.glb +0 -0
- cuju/render/web/assets/hair_short02.glb +0 -0
- cuju/render/web/assets/icons/cuju-192.png +0 -0
- cuju/render/web/assets/icons/cuju-512.png +0 -0
- cuju/render/web/assets/icons/cuju-maskable-512.png +0 -0
- cuju/render/web/assets/kit_goalkeeper.webp +0 -0
- cuju/render/web/assets/kit_mask.png +0 -0
- cuju/render/web/assets/kit_normal.webp +0 -0
- cuju/render/web/assets/manifest.json +601 -0
- cuju/render/web/assets/models/kaykit-barbarian.glb +0 -0
- cuju/render/web/assets/models/kaykit-barbarian.json +23 -0
- cuju/render/web/assets/models/kaykit-knight.glb +0 -0
- cuju/render/web/assets/models/kaykit-knight.json +23 -0
- cuju/render/web/assets/models/kaykit-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.glb +0 -0
- cuju/render/web/assets/models/kaykit-mannequin-medium.json +23 -0
- cuju/render/web/assets/models/kaykit-ranger.glb +0 -0
- cuju/render/web/assets/models/kaykit-ranger.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue-hooded.json +23 -0
- cuju/render/web/assets/models/kaykit-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-mage.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-minion.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-rogue.json +23 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.glb +0 -0
- cuju/render/web/assets/models/kaykit-skeleton-warrior.json +23 -0
- cuju/render/web/assets/pitch.webp +0 -0
- cuju/render/web/assets/pitch_normal.webp +0 -0
- cuju/render/web/assets/player.glb +0 -0
- cuju/render/web/assets/shoe.webp +0 -0
- cuju/render/web/assets/shoe_sole.png +0 -0
- cuju/render/web/assets/skin_01.png +0 -0
- cuju/render/web/assets/skin_02.png +0 -0
- cuju/render/web/assets/skin_03.png +0 -0
- cuju/render/web/assets/skin_04.png +0 -0
- cuju/render/web/assets/sounds/applause.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-1.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-2.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-3.ogg +0 -0
- cuju/render/web/assets/sounds/beach-wave-4.ogg +0 -0
- cuju/render/web/assets/sounds/city-horn.ogg +0 -0
- cuju/render/web/assets/sounds/city-loop.ogg +0 -0
- cuju/render/web/assets/sounds/city-siren.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-cheer.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-loop.ogg +0 -0
- cuju/render/web/assets/sounds/crowd-ooh.ogg +0 -0
- cuju/render/web/assets/sounds/gulls.ogg +0 -0
- cuju/render/web/assets/sounds/neon-sting.ogg +0 -0
- cuju/render/web/assets/sounds/rain-loop.ogg +0 -0
- cuju/render/web/assets/sounds/sounds.json +46 -0
- cuju/render/web/assets/sounds/space-beep.ogg +0 -0
- cuju/render/web/assets/sounds/space-hum.ogg +0 -0
- cuju/render/web/assets/sounds/space-radio.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-long.ogg +0 -0
- cuju/render/web/assets/sounds/whistle-short.ogg +0 -0
- cuju/render/web/assets/sounds/wind-loop.ogg +0 -0
- cuju/render/web/easy.css +357 -0
- cuju/render/web/export.html +23 -0
- cuju/render/web/feed.css +262 -0
- cuju/render/web/feed.html +28 -0
- cuju/render/web/flat.css +120 -0
- cuju/render/web/home.css +837 -0
- cuju/render/web/index.html +373 -0
- cuju/render/web/js/boot.js +50 -0
- cuju/render/web/js/broadcast/audio.js +2141 -0
- cuju/render/web/js/broadcast/celebrate.js +1238 -0
- cuju/render/web/js/broadcast/decisions.js +609 -0
- cuju/render/web/js/broadcast/hud.js +352 -0
- cuju/render/web/js/broadcast/show.js +3614 -0
- cuju/render/web/js/broadcast/transition.js +2231 -0
- cuju/render/web/js/broadcast/voices.js +153 -0
- cuju/render/web/js/capture.js +101 -0
- cuju/render/web/js/core/accessibility.js +90 -0
- cuju/render/web/js/core/api.js +429 -0
- cuju/render/web/js/core/comfort.js +135 -0
- cuju/render/web/js/core/i18n.js +705 -0
- cuju/render/web/js/core/icons.js +167 -0
- cuju/render/web/js/core/input-settings.js +156 -0
- cuju/render/web/js/core/keys.js +277 -0
- cuju/render/web/js/core/language-ui.js +125 -0
- cuju/render/web/js/core/menu.js +853 -0
- cuju/render/web/js/core/names.js +113 -0
- cuju/render/web/js/core/page-structure.js +110 -0
- cuju/render/web/js/core/scenarios.js +109 -0
- cuju/render/web/js/core/spoilers.js +236 -0
- cuju/render/web/js/core/typeahead.js +38 -0
- cuju/render/web/js/export/dom-picture.js +330 -0
- cuju/render/web/js/export/export-host.js +97 -0
- cuju/render/web/js/export/feed-dialog.js +191 -0
- cuju/render/web/js/export/feed-params.js +266 -0
- cuju/render/web/js/export/feed.js +363 -0
- cuju/render/web/js/export/mp4-mux.js +295 -0
- cuju/render/web/js/export/photo-core.js +313 -0
- cuju/render/web/js/export/photo.js +1314 -0
- cuju/render/web/js/export/safe-frame.js +59 -0
- cuju/render/web/js/export/size-fields.js +103 -0
- cuju/render/web/js/export/sizes.js +178 -0
- cuju/render/web/js/export/video-boot.js +55 -0
- cuju/render/web/js/export/video-browser.js +325 -0
- cuju/render/web/js/export/video-export.js +417 -0
- cuju/render/web/js/export/video-plan.js +207 -0
- cuju/render/web/js/export/video-range.js +185 -0
- cuju/render/web/js/export/webm-mux.js +198 -0
- cuju/render/web/js/feed-boot.js +151 -0
- cuju/render/web/js/feed-frame.js +101 -0
- cuju/render/web/js/help/doctor.js +488 -0
- cuju/render/web/js/help/firstrun.js +379 -0
- cuju/render/web/js/help/help-center.js +438 -0
- cuju/render/web/js/help/install-offer.js +164 -0
- cuju/render/web/js/help/install-state.js +104 -0
- cuju/render/web/js/help/lessons-core.js +207 -0
- cuju/render/web/js/help/lessons.js +463 -0
- cuju/render/web/js/help/palette.js +584 -0
- cuju/render/web/js/help/pwa.js +257 -0
- cuju/render/web/js/help/tour-core.js +241 -0
- cuju/render/web/js/help/tour.js +461 -0
- cuju/render/web/js/main.js +2513 -0
- cuju/render/web/js/pages/draw.js +188 -0
- cuju/render/web/js/pages/fieldpage.js +530 -0
- cuju/render/web/js/pages/home.js +521 -0
- cuju/render/web/js/pages/matches.js +779 -0
- cuju/render/web/js/pages/moments-ui.js +359 -0
- cuju/render/web/js/pages/queue-filters.js +63 -0
- cuju/render/web/js/pages/recording-groups.js +80 -0
- cuju/render/web/js/pages/recordings.js +1069 -0
- cuju/render/web/js/play/bench.js +347 -0
- cuju/render/web/js/play/challenges.js +1302 -0
- cuju/render/web/js/play/medals.js +127 -0
- cuju/render/web/js/play/play.js +1953 -0
- cuju/render/web/js/play/teasers.js +196 -0
- cuju/render/web/js/play/touch.js +350 -0
- cuju/render/web/js/play/tournament-layout.js +487 -0
- cuju/render/web/js/play/tournament-results.js +441 -0
- cuju/render/web/js/play/tournament-strict.js +560 -0
- cuju/render/web/js/play/tournament.js +2585 -0
- cuju/render/web/js/players/animate.js +972 -0
- cuju/render/web/js/players/clips.js +224 -0
- cuju/render/web/js/players/kaykit.js +162 -0
- cuju/render/web/js/players/kits.js +676 -0
- cuju/render/web/js/players/number-patch.js +82 -0
- cuju/render/web/js/players/players.js +2694 -0
- cuju/render/web/js/players/recolor.js +893 -0
- cuju/render/web/js/players/retarget.js +325 -0
- cuju/render/web/js/players/rig-numbers.js +174 -0
- cuju/render/web/js/presentation/animator.js +231 -0
- cuju/render/web/js/presentation/assign.js +85 -0
- cuju/render/web/js/presentation/behaviors.js +440 -0
- cuju/render/web/js/presentation/budget.js +209 -0
- cuju/render/web/js/presentation/catalog.js +1674 -0
- cuju/render/web/js/presentation/clocks.js +72 -0
- cuju/render/web/js/presentation/commands.js +301 -0
- cuju/render/web/js/presentation/contract.js +240 -0
- cuju/render/web/js/presentation/effects.js +904 -0
- cuju/render/web/js/presentation/flash.js +605 -0
- cuju/render/web/js/presentation/freeze-worklet.js +22 -0
- cuju/render/web/js/presentation/glsl.js +464 -0
- cuju/render/web/js/presentation/host.js +323 -0
- cuju/render/web/js/presentation/index.js +2066 -0
- cuju/render/web/js/presentation/loudness-worklet.js +235 -0
- cuju/render/web/js/presentation/loudness.js +740 -0
- cuju/render/web/js/presentation/moments.js +536 -0
- cuju/render/web/js/presentation/post.js +327 -0
- cuju/render/web/js/presentation/preview.js +180 -0
- cuju/render/web/js/presentation/protocol.js +174 -0
- cuju/render/web/js/presentation/reach.js +158 -0
- cuju/render/web/js/presentation/rigging.js +130 -0
- cuju/render/web/js/presentation/sandbox.js +157 -0
- cuju/render/web/js/presentation/shaders.js +235 -0
- cuju/render/web/js/presentation/stage.js +402 -0
- cuju/render/web/js/presentation/teamaudio.js +722 -0
- cuju/render/web/js/presentation/tracks.js +197 -0
- cuju/render/web/js/presentation/values.js +81 -0
- cuju/render/web/js/recording/annotation-data.js +203 -0
- cuju/render/web/js/recording/captions.js +491 -0
- cuju/render/web/js/recording/codec.js +185 -0
- cuju/render/web/js/recording/decode-worker.js +70 -0
- cuju/render/web/js/recording/decode.js +94 -0
- cuju/render/web/js/recording/episodes.js +46 -0
- cuju/render/web/js/recording/format.js +461 -0
- cuju/render/web/js/recording/live.js +348 -0
- cuju/render/web/js/recording/moments-query.js +59 -0
- cuju/render/web/js/recording/moments.js +149 -0
- cuju/render/web/js/recording/pose.js +86 -0
- cuju/render/web/js/recording/timeline.js +275 -0
- cuju/render/web/js/teams/art.js +50 -0
- cuju/render/web/js/teams/clip-editor.js +293 -0
- cuju/render/web/js/teams/editor.js +1979 -0
- cuju/render/web/js/teams/library.js +344 -0
- cuju/render/web/js/teams/painter.js +633 -0
- cuju/render/web/js/teams/surprise-ui.js +195 -0
- cuju/render/web/js/teams/surprise.js +497 -0
- cuju/render/web/js/teams/team-check.js +127 -0
- cuju/render/web/js/watch/annotations.js +270 -0
- cuju/render/web/js/watch/flat.js +366 -0
- cuju/render/web/js/watch/grid.js +436 -0
- cuju/render/web/js/watch/pitch2d.js +289 -0
- cuju/render/web/js/watch/player-tools.js +192 -0
- cuju/render/web/js/watch/presets.js +201 -0
- cuju/render/web/js/world/arenas.js +997 -0
- cuju/render/web/js/world/ball.js +204 -0
- cuju/render/web/js/world/cameras.js +982 -0
- cuju/render/web/js/world/crowd.js +376 -0
- cuju/render/web/js/world/field.js +227 -0
- cuju/render/web/js/world/framing.js +145 -0
- cuju/render/web/js/world/occlusion.js +48 -0
- cuju/render/web/js/world/pitch.js +703 -0
- cuju/render/web/js/world/scene.js +395 -0
- cuju/render/web/js/world/sky.js +325 -0
- cuju/render/web/js/world/stadium.js +1262 -0
- cuju/render/web/js/world/vendor-sky.js +367 -0
- cuju/render/web/js/world/weather.js +264 -0
- cuju/render/web/languages/en.json +4750 -0
- cuju/render/web/lessons.json +137 -0
- cuju/render/web/manifest.webmanifest +23 -0
- cuju/render/web/style.css +8399 -0
- cuju/render/web/sw.js +235 -0
- cuju/render/web/vendor/fabric/LICENSE +16 -0
- cuju/render/web/vendor/fabric/fabric.min.mjs +1 -0
- cuju/render/web/vendor/lucide/LICENSE +43 -0
- cuju/render/web/vendor/lucide/icons.js +141 -0
- cuju/render/web/vendor/three/LICENSE +21 -0
- cuju/render/web/vendor/three/build/three.core.js +60586 -0
- cuju/render/web/vendor/three/build/three.module.js +19719 -0
- cuju/render/web/vendor/three/examples/jsm/loaders/GLTFLoader.js +4925 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/EffectComposer.js +365 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/MaskPass.js +195 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/OutputPass.js +148 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/Pass.js +191 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/RenderPass.js +193 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/ShaderPass.js +135 -0
- cuju/render/web/vendor/three/examples/jsm/postprocessing/UnrealBloomPass.js +541 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/CopyShader.js +52 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/LuminosityHighPassShader.js +68 -0
- cuju/render/web/vendor/three/examples/jsm/shaders/OutputShader.js +103 -0
- cuju/render/web/vendor/three/examples/jsm/utils/BufferGeometryUtils.js +1501 -0
- cuju/render/web/vendor/three/examples/jsm/utils/SkeletonUtils.js +496 -0
- cuju_render-0.1.0.dist-info/METADATA +116 -0
- cuju_render-0.1.0.dist-info/RECORD +382 -0
- cuju_render-0.1.0.dist-info/WHEEL +4 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSE +202 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC-BY-4.0.txt +396 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/CC0-1.0.txt +127 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/ISC.txt +16 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/MIT.txt +24 -0
- cuju_render-0.1.0.dist-info/licenses/LICENSES/Unlicense.txt +13 -0
- 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,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
|