Skip to content

Commit

Permalink
Use rich cli in runserver
Browse files Browse the repository at this point in the history
  • Loading branch information
piercefreeman committed Apr 29, 2024
1 parent cf70733 commit 43ac126
Show file tree
Hide file tree
Showing 12 changed files with 346 additions and 144 deletions.
93 changes: 71 additions & 22 deletions ci_webapp/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 35 additions & 10 deletions mountaineer/cli.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import asyncio
import socket
from contextlib import contextmanager
from contextlib import contextmanager, redirect_stdout
from dataclasses import dataclass
from functools import partial
from importlib import import_module
from importlib.metadata import distributions
from io import StringIO
from multiprocessing import Event, Process, Queue, get_start_method, set_start_method
from multiprocessing.queues import Queue as QueueType
from pathlib import Path
from signal import SIGINT, signal
from tempfile import mkdtemp
from threading import Thread
from time import sleep, time
from time import monotonic_ns, sleep, time
from traceback import format_exception
from typing import Any, Callable, MutableMapping

Expand All @@ -20,6 +21,7 @@

from mountaineer.app import AppController
from mountaineer.client_builder.builder import ClientBuilder
from mountaineer.console import CONSOLE, ERROR_CONSOLE
from mountaineer.controllers.exception_controller import ExceptionController
from mountaineer.js_compiler.exceptions import BuildProcessException
from mountaineer.logging import LOGGER
Expand Down Expand Up @@ -89,7 +91,23 @@ def run(self):
f"Starting isolated environment process with\nbuild_config: {self.build_config}\nrunserver_config: {self.runserver_config}"
)

app_controller = import_from_string(self.build_config.webcontroller)
CONSOLE.rule("[bold red]Mountaineer Build Started")

with (
# We don't want to print stdout on the initial import, since this will just duplicate
# the init code / logging of the app. We use our error console to avoid
# capturing the stdout of our logging
ERROR_CONSOLE.status("[bold blue]Loading app...", spinner="dots"),
StringIO() as buf,
redirect_stdout(buf),
):
start = monotonic_ns()
app_controller = import_from_string(self.build_config.webcontroller)
LOGGER.debug(f"Load app logs: {buf.getvalue()}")
CONSOLE.print(
f"[bold green]🎒 Loaded app in {(monotonic_ns() - start) / 1e9:.2f}s"
)

if not isinstance(app_controller, AppController):
raise ValueError(
f"Expected {self.build_config.webcontroller} to be an instance of AppController"
Expand Down Expand Up @@ -132,7 +150,7 @@ def run(self):
thread.stop()
thread.join()

LOGGER.debug("IsolatedEnvProcess finished")
LOGGER.debug("IsolatedEnvProcess finished")

def rebuild_js(self):
LOGGER.debug("JS-Only rebuild started")
Expand Down Expand Up @@ -200,7 +218,6 @@ def wait_for_rebuild():
rebuild_thread.start()

def run_build(self, app_controller: AppController):
secho("Starting build...", fg="yellow")
start = time()
js_compiler = ClientBuilder(
app_controller,
Expand All @@ -213,14 +230,16 @@ def run_build(self, app_controller: AppController):
)
try:
js_compiler.build()
secho(f"Build finished in {time() - start:.2f} seconds", fg="green")
CONSOLE.print(
f"[bold green]🚀 App launched in {time() - start:.2f} seconds"
)

# Completed successfully
app_controller.build_exception = None

self.alert_notification_channel()
except BuildProcessException as e:
secho(f"Build failed: {e}", fg="red")
CONSOLE.print(f"[bold red]⚠️ Build failed: {e}")
app_controller.build_exception = e

def stop(self, hard_timeout: float = 5.0):
Expand Down Expand Up @@ -420,7 +439,7 @@ def handle_build(
)
start = time()
js_compiler.build()
secho(f"Build finished in {time() - start:.2f} seconds", fg="green")
CONSOLE.print(f"[bold green]App built in {time() - start:.2f}s")


def update_multiprocessing_settings():
Expand Down Expand Up @@ -516,12 +535,18 @@ def build_common_watchdog(
def init_global_state(webcontroller: str):
"""
Initialize global state: signal to each builder that they can
initialize global state before the fork.
set up their own global state before the fork.
"""
global_state: dict[Any, Any] = {}

app_controller = import_from_string(webcontroller)
with (
ERROR_CONSOLE.status("[bold blue]Setting up global state before fork...", spinner="dots"),
StringIO() as buf,
redirect_stdout(buf),
):
app_controller = import_from_string(webcontroller)
LOGGER.debug(f"init_global_state Load app logs: {buf.getvalue()}")

if not isinstance(app_controller, AppController):
raise ValueError(f"Unknown app controller: {app_controller}")
Expand Down
43 changes: 24 additions & 19 deletions mountaineer/client_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
TSLiteral,
python_payload_to_typescript,
)
from mountaineer.console import CONSOLE
from mountaineer.controller import ControllerBase
from mountaineer.io import gather_with_concurrency
from mountaineer.js_compiler.base import ClientBundleMetadata
Expand Down Expand Up @@ -70,26 +71,30 @@ def build(self):
async def async_build(self):
# Avoid rebuilding if we don't need to
if self.cache_is_outdated():
secho("Building useServer, cache outdated...", fg="green")

# Make sure our application definitions are in a valid state before we start
# to build the client code
self.validate_unique_paths()

# Static files that don't depend on client code
self.generate_static_files()

# The order of these generators don't particularly matter since most TSX linters
# won't refresh until they're all complete. However, this ordering better aligns
# with semantic dependencies so we keep the linearity where possible.
self.generate_model_definitions()
self.generate_action_definitions()
self.generate_link_shortcuts()
self.generate_link_aggregator()
self.generate_view_servers()
self.generate_index_file()
start = monotonic_ns()

with CONSOLE.status("Building useServer", spinner="dots"):
# Make sure our application definitions are in a valid state before we start
# to build the client code
self.validate_unique_paths()

# Static files that don't depend on client code
self.generate_static_files()

# The order of these generators don't particularly matter since most TSX linters
# won't refresh until they're all complete. However, this ordering better aligns
# with semantic dependencies so we keep the linearity where possible.
self.generate_model_definitions()
self.generate_action_definitions()
self.generate_link_shortcuts()
self.generate_link_aggregator()
self.generate_view_servers()
self.generate_index_file()
CONSOLE.print(
f"[bold green]🔨 Built useServer in {(monotonic_ns() - start) / 1e9:.2f}s"
)
else:
secho("useServer up to date", fg="green")
CONSOLE.print("[bold green]useServer up to date")

await self.build_javascript_chunks()

Expand Down
6 changes: 6 additions & 0 deletions mountaineer/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Separated from logging.py to isolate build-time dependencies
# from ones required by actual deployments
from rich.console import Console

CONSOLE = Console()
ERROR_CONSOLE = Console(stderr=True)
56 changes: 41 additions & 15 deletions mountaineer/js_compiler/javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from typing import Any

from inflection import underscore
from rich.progress import Progress, SpinnerColumn, TimeElapsedColumn

from mountaineer import mountaineer as mountaineer_rs # type: ignore
from mountaineer.console import CONSOLE
from mountaineer.controller import ControllerBase
from mountaineer.js_compiler.base import ClientBuilderBase, ClientBundleMetadata
from mountaineer.js_compiler.exceptions import BuildProcessException
Expand Down Expand Up @@ -105,23 +107,47 @@ def process_pending_files(
]

start = monotonic_ns()
try:
# TODO: Right now this raises pyo3_runtime.PanicException, which isn't caught
# appropriately. Our try/except block should be catching this.
mountaineer_rs.build_javascript(build_params)
except Exception as e:
LOGGER.error(f"Error building JS: {e}")
output_queue.put(
CompiledOutput(
success=False,
exception_type=e.__class__.__name__,
exception_message=str(e),
)

with Progress(
SpinnerColumn(),
*Progress.get_default_columns(),
TimeElapsedColumn(),
console=CONSOLE,
transient=True,
) as progress:
build_task = progress.add_task(
"[cyan]Compiling...", total=len(build_params)
)
continue

LOGGER.debug(
f"Processed payload in {(monotonic_ns() - start) / 1e9} seconds"
try:

def build_complete_callback(callback_args: tuple[int]):
"""
Callback called when each individual file build is complete. For a successful
build this callback |N| should match the input build_params.
"""
progress.advance(build_task, 1)

# Right now this raises pyo3_runtime.PanicException, which isn't caught
# appropriately. Our try/except block should be catching this.
mountaineer_rs.build_javascript(
build_params, build_complete_callback
)

except Exception as e:
LOGGER.error(f"Error building JS: {e}")
output_queue.put(
CompiledOutput(
success=False,
exception_type=e.__class__.__name__,
exception_message=str(e),
)
)
continue

CONSOLE.print(
f"[bold green]📬 Compiled frontend in {(monotonic_ns() - start) / 1e9:.2f}s"
)

output_queue.put(CompiledOutput(success=True))
Expand Down
Loading

0 comments on commit 43ac126

Please sign in to comment.