Runtime Builds¶
Static Exports¶
In some cases, you might want to export your application as static HTML. This makes it much easier to serve your app somewhere, at the limit of being able to perform actions server-side. You can export your app in view.py via the view build
command, or by running the build_app
function:
$ view build
* Starting build process!
* Starting build steps
* Getting routes
* Calling GET /...
* Created ...
* Created index.html
* Successfully built app
This will export your app into a static folder called build
, which can then be served via something like http.server. An exported route cannot contain:
- Route Inputs
- Path Parameters
- A method other than
GET
As stated above, you can also build your app programatically via build_app
:
from view import new_app
from view.build import build_app
app = new_app()
app.load() # Call the loader manually, since we aren't calling run()
build_app(app)
Build Steps¶
Instead of exporting static HTML, you might just want to call some build script at runtime for your app to use. For example, this could be something like a Next.js app, which you want to use as the UI for your website. Each different build is called a build step in View. View's build system does not aim to be a full fledged build system, but instead a bridge to use other package managers or tools to build requirements for your app. It tries to be extendable, instead of batteries-included.
To specify a build step, add it under build.steps
in your configuration. A build step should contain a list of requirements under requires
and a command
:
By default, this will only be run once the app is started. If you would like to run it every time a certain route is called, add the steps
parameter to a router function. Note that this will make your route much slower (as a build process needs to be started for every request), so it's highly recommended that you cache the route.
For example:
from view import new_app
app = new_app()
@app.get("/", steps=["nextjs"], cache_rate=10000) # Reloads app every 10,000 requests
async def index():
return await app.template("out/index.html")
app.run()
Executing Build Scripts¶
Instead of running a command, you can also run a Python script. To do this, simply specify a script
value as a path to a file instead of a command
:
Note
__name__
is set to __view_build__
when using a build script. If you want to use the file for other things, you can simply check if __name__ == "__view_build__"
You can also specify a list of files or commands for both, to run multiple of either:
# view.toml
[build.steps.foo]
requires = ["gcc"]
script = ["foo.py", "bar.py"]
command = ["gcc -c -Wall -Werror -fpic foo.c", "gcc -shared -o libfoo.so foo.o"]
If the script needs to run asynchronous code, export a __view_build__
from the script:
# build.py
import aiofiles
# This function will be run by the view.py build system
async def __view_build__():
async with aiofiles.open("something.txt", "w") as f:
await f.write("...")
Default Steps¶
As said earlier, the default build steps are always run right before the app is started, and then never ran again (unless explicitly needed by a route). If you would like only certain steps to run, specify them with the build.default_steps
value:
# view.toml
[build]
default_steps = ["nextjs"]
# Only NextJS will be built on startup
[build.steps.nextjs]
requires = ["npm"]
command = "npm run build"
[build.steps.php]
requires = ["php"]
command = "php -f payment.php"
Platform-Dependent Steps¶
Many commands are different based on the platform used. For example, to read from a file on the Windows shell would be type
, while on Linux and Mac it would be cat
. If you add multiple step entries (in the form of an array of tables) with platform
values, view.py will run the entry based on the platform the app was run on.
For example, using the file reading example from above:
Notice the double brackets next to [[build.steps.read_from_file]]
, specifying an array of tables.
# view.toml
[[build.steps.read_from_file]]
platform = ["mac", "linux"]
command = "cat whatever.txt"
[[build.steps.read_from_file]]
platform = "windows"
command = "type whatever.txt"
The platform
value can be one of three things per entry:
- A list of platforms.
- A string containing a single platform.
None
, meaning to use this entry if no other platforms match.
For example, with a None
platform set (on multiple entries), the above could be rewritten as:
# view.toml
[[build.steps.read_from_file]]
# Windows ONLY runs this step
platform = "windows"
command = "type whatever.txt"
[[build.steps.read_from_file]]
# All other platforms run this!
command = "cat whatever.txt"
Note that only one step entry can have a None
platform value, otherwise view.py will throw an error.
Note
The only recognized operating systems for platform
are the big three: Windows, Mac, and any Linux based system. If you want more fine-grained control (for example, using pacman
or apt
depending on the Linux distro), use a custom build script that knows how to read the Linux distribution.
Build Requirements¶
As you've seen above, build requirements are specified via the requires
value. Out of the box, view.py supports a number of different build tools, compilers, and interpreters. To specify a requirement for one, simply add the name of their executable (i.e., how you access their CLI). For example, since pip
is accessed via using the pip
command in your terminal, pip
is the name of the requirement.
However, view.py might not support checking for a command by default (this is the case if you get a Unknown build requirement
error). If so, you need a custom requirement. If you would like to, you can make an issue requesting support for it as well.
Custom Requirements¶
There are four types of custom requirements, which are specified by adding a prefix to the requirement name:
- Importing a Python module (
mod+
) - Executing a Python script (
script+
) - Checking if a path exists (
path+
) - Checking if a command exists (
command+
)
For example, the command+gcc
would make sure that gcc --version
return 0
:
The Requirement Protocol¶
In a custom requirement specifying a module or script, view.py will attempt to call an asynchronous __view_requirement__
function (similar to __view_build__
). This function should return a bool
value, with True
indicating that the requirement exists, and False
otherwise.
Note
If no __view_requirement__
function exists, then all view.py does it check that execution or import was successful, and marks the requirement as passing.
For example, if you were to write a requirement script that checks if the Python version is at least 3.10
, it could look like:
# check_310.py
import sys
async def __view_requirement__() -> bool:
# Make sure we're running on at least Python 3.10
return sys.version_info >= (3, 10)
The above could actually be used via both script+check_310.py
and mod+check_310
.
Note
Don't use the view.py build system to check the Python version or if a Python package is installed. Instead, use the dependencies
section of a pyproject.toml
file, or PEP 723 script metadata.
Review¶
View can build static HTML with the view build
command, or via view.build.build_app
. Build steps in view.py are used to call external build systems, which can then in turn be used to build things your app needs at runtime (such as static HTML generated by Next.js). Builds can run commands, Python scripts, or both.
Each build step contains a list of build requirements. View provides several known requirements to specify out of the box, but you may also specify custom requirements, either via a Python script or module, checking a file path, or executing an arbitrary command.