Build Process
Our "client builder" collection of classes manages generating the Typescript definitions from
the defined Python controllers. While this component doesn't affect the actual value of the parsed
values retrieved from the fetched()
API at runtime, it's critical for development (IDE typehints
and CI static typechecking). Working with type interfaces that are always in sync with the backend
is one of the core benefits of using Mountaineer.
Note that this documentation is mostly intended for core maintainers who are working on the Mountaineer codebase, versus on projects that use Mountaineer.
Evolution
In our 0.1-0.8 releases, the client builder operated on the OpenAPI schema that was generated from the API server. This ensured that definitions always matched the JS-like typing of the OpenAPI. There were some limitations to this approach:
- Loss of inheritance tree: Each controller provided flat functions into the OpenAPI schema, without any of their class inheritance structure. This made it impossible to define function signatures in TS that relied on a common ancessor without specifying all of the downstream client subclasses.
- Formatting of enum: Enums only define their values within OpenAPI, not their Python names.
We would build up a synthetic enum key/value pair based on the string value but in cases
where key/value in Python were meaningfully different (e.g.
Enum("foo", "bar")
), we would lose that information.
In 0.9 we moved to a new approach that uses the Python controllers directly to generate the TypeScript definitions. This corrects all of the drawbacks of the OpenAPI approach in addition to cleaning up the code to more easily make extensions in the future.
Our new architecture is structured like so:
In-Memory Controllers: Controllers are loaded into memory from their disk files so we have full access to their code signatures and runtime state.
Parser: Extracts all metadata of the controllers/models necessary to generate TypeScript definitions. These are stored in intermediate data classes.
- ControllerWrapper
- FieldWrapper
- ActionWrapper
- ModelWrapper
- EnumWrapper
InterfaceBuilder: Takes the metadata and generates isolated TypeScript definitions. These are separated by output type (interface vs. enum for instance) so we can compartmentalize the string construction of frontend files.
- ControllerInterface
- ModelInterface
- EnumInterface
- ActionInterface
FileGenerators: Write the full TypeScript files to disk, which often requires collecting multiple interfaces, tracking their imports, and assembling into a single file
App Isolation
When you run our development server via runserver
, we spawn your app in a new process. This
process is isolated from the main runserver process by a message broker. This allows us to send messages
to the app to reload code, restart the server, or shutdown the app.
For small projects, performing a full process restart can be sufficient to update code files. But as projects grow larger, the bootup time for Python to read all your modules into memory can become meaningful. Mountaineer works around this limitation by implementing a hot-reloader. Our hot reloader waits for changes to files on disk and then reloads the changed modules in-memory without a full server restart. We do a best-effort approach to build up a DAG of dependencies between modules so we can reload them in the correct order.
There are some cases, however, where we can't naively reload a module. One common case is in SQLAlchemy where Table definitions can only be added once to the global registry. If we were to reload a module that added a table to the registry, it raises a runtime exception. In these cases our dev app manager will perform a full server restart by shutting down the old process and starting a new one.
Regardless of the approach, the main process will wait for the new server to boot up and then send a notification
to the frontend via our websocket server WatcherWebservice
.
::: mountaineer.development.manager.DevAppManager
Manages the app controller, server, and compilation.
Class Constructor
- Name
package
- Type
- str
- Description
Name of the main package
- Name
package_path
- Type
- Path
- Description
Path to the package on disk
- Name
module_name
- Type
- str
- Description
Module name containing the app controller
- Name
controller_name
- Type
- str
- Description
Variable name of the app controller within the module
Class Attributes
- Name
package
- Type
- Description
- Name
package_path
- Type
- Description
- Name
module_name
- Type
- Description
- Name
controller_name
- Type
- Description
- Name
webservice_thread
- Type
- UvicornThread | None
- Description
- Name
app_controller
- Type
- AppController | None
- Description
- Name
js_compiler
- Type
- APIBuilder | None
- Description
- Name
app_compiler
- Type
- ClientCompiler | None
- Description
Class Methods
- Name
from_webcontroller
- Return type
- Description
- Name
run_async
- Return type
- Description
Asynchronous main loop that processes messages from the broker.
This is the core message handling loop that:
- Retrieves messages from the queue
- Dispatches them to appropriate handlers
- Sends responses back through the broker
The loop continues until a ShutdownMessage is received.
- Name
handle_build_use_server
- Return type
- Description
Build the useServer support files for client-side React Server Components.
- Name
handle_js_build
- Return type
- Description
Compile JavaScript files based on updated paths.
Runs the builder plugins and invalidates any affected views in the app controller.
- Name
initialize_app_state
- Return type
- Description
Initialize all application state components within the isolated context.
This method:
- Loads the web service and app controller
- Initializes the hot reloader
- Mounts the exception controller for development error pages
- Sets up JS and app compilers
- Name
handle_start_server
- Return type
- Description
Start the Uvicorn server for the web application.
Configures and launches a UvicornThread to serve the application. If a server is already running, it will be stopped first.
- Name
mount_exceptions
- Return type
- Description
Mount the exception controller to the app controller for custom error pages.
This adds development-friendly error pages with stack traces and debugging information. It ensures the exception controller is only mounted once.
- Name
handle_dev_exception
- Return type
- Description
Handle exceptions in development mode with enhanced error pages.
For GET requests, renders a detailed error page with stack traces and debugging info. For other request types, re-raises the exception to be handled by the framework.
::: mountaineer.development.hotreload.HotReloader
Monitors Python packages for file changes and triggers registered callbacks when changes occur.
Manages the lifecycle of file watching across multiple packages, optimizing the watch paths to avoid redundant watchers, and verifying package availability. Used as the foundation for hot-reloading and development tooling in Mountaineer.
Class Constructor
- Name
main_package
- Type
- str
- Description
Primary package to monitor for file changes
- Name
dependent_packages
- Type
- list[str]
- Description
Additional packages to monitor for changes
- Name
callbacks
- Type
- list[CallbackDefinition] | None
- Description
Default: None
List of callback definitions to execute when changes occur
- Name
run_on_bootup
- Type
- bool
- Description
Default: False
Typically, we will only notify callback if there has been a change to the filesystem. If this is set to True, we will run all callbacks on bootup as well.
Class Attributes
- Name
main_package
- Type
- Description
- Name
packages
- Type
- Description
- Name
paths
- Type
- list[str]
- Description
- Name
callbacks
- Type
- list[CallbackDefinition]
- Description
- Name
run_on_bootup
- Type
- Description
- Name
stop_event
- Type
- Description
- Name
running
- Type
- Description
Class Methods
- Name
start_watching
- Return type
- Description
Begin asynchronously watching all package paths for file changes.
If configured with run_on_bootup=True, immediately runs all callbacks once. Sets up the FileWatcher with registered callbacks and processes changes as they occur on the filesystem.
- Name
stop_watching
- Return type
- Description
Stop watching all file paths and reset the watchdog state.
Signals the underlying watchfiles library to stop watching by setting the stop event and resets internal state for potential restart.
- Name
check_packages_installed
- Return type
- Description
Verify that all packages being watched are installed in the current environment.
- Name
get_package_paths
- Return type
- Description
Resolve filesystem paths for all monitored packages.
Finds each package's location on disk using importlib and optimizes the path list to eliminate redundant watchers through merge_paths.
- Name
merge_paths
- Return type
- list[str]
- Description
Optimize the list of paths by removing subdirectories when their parent is already watched.
If one path is a subdirectory of another, we only want to watch the parent directory. This function merges the paths to avoid duplicate watchers.
Code
import asyncio from mountaineer.development.watch import PackageWatchdog, CallbackDefinition, CallbackType, CallbackMetadata # Define a callback function to handle file changes async def reload_modules(metadata: CallbackMetadata) -> None: print(f"Changes detected in {len(metadata.events)} files") for event in metadata.events: print(f" {event.action.name}: {event.path}") # You would typically reload modules or trigger other actions here # Create a watchdog for your main package and any dependencies watchdog = PackageWatchdog( main_package="my_app", dependent_packages=["my_library"], callbacks=[ CallbackDefinition( action=CallbackType.MODIFIED | CallbackType.CREATED, callback=reload_modules ) ], run_on_bootup=True ) await watchdog.start_watching()
A simple webserver to notify frontends about updated builds via WebSockets.
The WatcherWebservice provides a multiprocessing safe queue that's
accessible as notification_queue
. Each time that a process
wants to update the frontend, it can push a message into the queue.
This service is a critical component in the hot-reloading architecture, enabling backend changes to trigger frontend refreshes without manual intervention. It runs as a lightweight FastAPI application in a separate thread.
Class Constructor
- Name
webservice_host
- Type
- str
- Description
The host address to bind the webservice to (e.g. '127.0.0.1')
- Name
webservice_port
- Type
- int | None
- Description
Default: None
Optional port number to use. If not provided, a free port will be allocated.
Class Attributes
- Name
app
- Type
- Description
- Name
websockets
- Type
- list[WebSocket]
- Description
- Name
host
- Type
- Description
- Name
port
- Type
- Description
- Name
notification_queue
- Type
- Queue[bool | None]
- Description
- Name
webservice_thread
- Type
- UvicornThread | None
- Description
- Name
monitor_build_thread
- Type
- Thread | None
- Description
- Name
has_started
- Type
- Description
Class Methods
- Name
build_app
- Return type
- Description
Construct the FastAPI application with necessary routes and WebSocket endpoints.
Creates a simple API with:
- A root endpoint returning basic service information
- A WebSocket endpoint at "/build-events" for real-time build notifications
- Name
broadcast_listeners
- Return type
- Description
Send a notification to all connected WebSocket clients.
This method is called when a new build is available, signaling all connected frontends to refresh.
- Name
monitor_builds
- Return type
- Description
Monitor the notification queue and broadcast updates to connected clients.
This method runs in a separate thread, blocking on the notification queue and calling broadcast_listeners when a notification is received. The thread terminates when None is pushed to the queue.
- Name
start
- Return type
- Description
Start the WatcherWebservice by launching the necessary threads.
Initializes and starts:
- A UvicornThread to serve the FastAPI application
- A monitor thread to watch for build notifications
- Name
stop
- Return type
- bool
- Description
Attempts to stop the separate WatcherWebservice threads.
Sends termination signals to the Uvicorn server and monitor threads, then waits for them to complete within the specified timeout period.
::: mountaineer.development.messages.AsyncMessageBroker