Skip to content

Commit 7de6ec8

Browse files
Merge pull request #171 from ZeroIntensity/build-step
Build Steps
2 parents 46d7f2d + 3e5d348 commit 7de6ec8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+2130
-1786
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ a.md
2626
new_app/
2727
vgcore.*
2828
.vscode/
29+
*.test

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Added a startup message
1515
- Added support for `daphne` and `hypercorn` as servers
1616
- Added documentation for `view.env` and added environment variables to configuration
17-
- Removed dead file `src/view/nodes.py`
17+
- Removed dead file `src/view/nodes.py` and `src/view/compiler.py`
1818
- Added `patterns` loader to `view init`
1919
- Updated internal C API structure
20+
- Added `build` to config
21+
- Added the `build_app` and `build_steps` functions
22+
- `Route.__call__` is now used internally over `Route.func`
23+
- Added the `to_response` function
24+
- Improved type checking on functions decorated with a router function
25+
- Added preset values to the `view.toml` generated by `view init`
26+
- Fixed fancy logging not exiting after a `KeyboardInterrupt`
27+
- Added prettier input prompts to `view init`
28+
- Added `HTML.from_file`
29+
- **Breaking Change:** Middleware functions must now take `call_next`
2030

2131
## [1.0.0-alpha9] - 2024-2-4
2232

CONTRIBUTING.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async def index():
6262
app.run()
6363
```
6464

65-
**Note:** Import from `view` internally *does not* work when using `src.view`. Instead, your imports inside of view.py should look like `from .foo import bar`. For example, if you wanted to import `view.routing.get` from `src/view/app.py`, your import would look like `from .routing import get`
65+
**Note:** Import from `view` internally _does not_ work when using `src.view`. Instead, your imports inside of view.py should look like `from .foo import bar`. For example, if you wanted to import `view.routing.get` from `src/view/app.py`, your import would look like `from .routing import get`
6666

6767
For debugging purposes, you're also going to want to disable `fancy` and `hijack` in the configuration:
6868

@@ -78,13 +78,12 @@ These settings will stop view.py's fancy output from showing, as well as stoppin
7878

7979
**Note:** You do need to `pip install .` to update the tests, as they import from `view` and not `src.view`.
8080

81-
View uses [ward](https://ward.readthedocs.io/en/latest/) for writing tests. If you have any questions regarding test semantics, it's likely on their docs. The only thing you need to understand for writing tests is how to use the `App.test` API.
81+
View uses [pytest](https://docs.pytest.org/en/8.2.x/) for writing tests, as well as [pytest-asyncio](https://pytest-asyncio.readthedocs.io/en/latest/) and [pytest-memray](https://pytest-memray.readthedocs.io/en/latest/). If you have any questions regarding test semantics, it's likely on their docs. The only thing you need to understand for writing tests is how to use the `App.test` API.
8282

8383
`App.test` is a method that lets you start a virtual server for testing responses. It works like so:
8484

8585
```py
86-
@test("my cool feature")
87-
async def _():
86+
async def test_my_feature():
8887
app = new_app()
8988

9089
@app.get("/")

README.md

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,80 @@
33
<div align="center"><h2>The Batteries-Detachable Web Framework</h2></div>
44

55
> [!Warning]
6-
> view.py is in very early stages and not yet considered to be ready for production.
7-
> If you would like to follow development progress, join [the discord](https://discord.gg/tZAfuWAbm2).
8-
> For contributing to view.py, please see our [CONTRIBUTING.md](https://github.com/ZeroIntensity/view.py/blob/master/CONTRIBUTING.md)
6+
> view.py is currently in alpha, and may be lacking some features.
7+
> If you would like to follow development progress, be sure to join [the discord](https://discord.gg/tZAfuWAbm2).
98
109
<div align="center">
1110
<a href="https://clientarea.space-hosting.net/aff.php?aff=303"><img width=150 height=auto src="https://cdn-dennd.nitrocdn.com/fygsTSpFNuiCdXWNTtgOTVMRlPWNnIZx/assets/images/optimized/rev-758b0f8/www.space-hosting.net/wp-content/uploads/2023/02/cropped-Icon.png"></a>
1211
<h3>view.py is affiliated with <a href="https://clientarea.space-hosting.net/aff.php?aff=303">Space Hosting</a></h3>
1312
</div>
1413

15-
- [Docs](https://view.zintensity.dev)
16-
- [Source](https://github.com/ZeroIntensity/view.py)
17-
- [PyPI](https://pypi.org/project/view.py)
18-
- [Discord](https://discord.gg/tZAfuWAbm2)
14+
- [Docs](https://view.zintensity.dev)
15+
- [Source](https://github.com/ZeroIntensity/view.py)
16+
- [PyPI](https://pypi.org/project/view.py)
17+
- [Discord](https://discord.gg/tZAfuWAbm2)
1918

20-
## Example
19+
## Features
20+
21+
- Batteries Detachable: Don't like our approach to something? No problem! We aim to provide native support for all your favorite libraries, as well as provide APIs to let you reinvent the wheel as you wish.
22+
- Lightning Fast: Powered by [pyawaitable](https://github.com/ZeroIntensity/pyawaitable), view.py is the first web framework to implement ASGI in pure C, without the use of external transpilers.
23+
- Developer Oriented: view.py is developed with ease of use in mind, providing a rich documentation, docstrings, and type hints.
24+
25+
See [why I wrote it](https://view.zintensity.dev/#why-did-i-build-it) on the docs.
26+
27+
## Examples
2128

2229
```py
23-
from view import new_app, h1
30+
from view import new_app
2431

2532
app = new_app()
2633

2734
@app.get("/")
2835
async def index():
29-
return h1("Hello, view.py!")
36+
return await app.template("index.html", engine="jinja")
3037

3138
app.run()
3239
```
3340

41+
```py
42+
# routes/index.py
43+
from view import get, HTML
44+
45+
# Build TypeScript Frontend
46+
@get(steps=["typescript"], cache_rate=1000)
47+
async def index():
48+
return await HTML.from_file("dist/index.html")
49+
```
50+
51+
```py
52+
from view import JSON, body, post
53+
54+
@post("/create")
55+
@body("name", str)
56+
@body("books", dict[str, str])
57+
def create(name: str, books: dict[str, str]):
58+
# ...
59+
return JSON({"message": "Successfully created user!"}), 201
60+
```
61+
3462
## Installation
3563

3664
**Python 3.8+ is required.**
3765

38-
### Development
66+
### Development
3967

40-
```
68+
```console
4169
$ pip install git+https://github.com/ZeroIntensity/view.py
4270
```
4371

44-
### Linux/macOS
72+
### PyPI
4573

46-
```
47-
$ python3 -m pip install -U view.py
74+
```console
75+
$ pip install view.py
4876
```
4977

50-
### Windows
78+
### Pipx
5179

52-
```
53-
> py -3 -m pip install -U view.py
80+
```console
81+
$ pipx install view.py
5482
```

docs/building-projects/build_steps.md

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Runtime Builds
2+
3+
## Static Exports
4+
5+
In some cases, you might want to export your application as [static HTML](https://en.wikipedia.org/wiki/Static_web_page). 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:
6+
7+
```
8+
$ view build
9+
* Starting build process!
10+
* Starting build steps
11+
* Getting routes
12+
* Calling GET /...
13+
* Created ...
14+
* Created index.html
15+
* Successfully built app
16+
```
17+
18+
This will export your app into a static folder called `build`, which can then be served via something like [http.server](https://docs.python.org/3/library/http.server.html). An exported route cannot contain:
19+
20+
- Route Inputs
21+
- Path Parameters
22+
- A method other than `GET`
23+
24+
As stated above, you can also build your app programatically via `build_app`:
25+
26+
```py
27+
from view import new_app
28+
from view.build import build_app
29+
30+
app = new_app()
31+
app.load() # Call the loader manually, since we aren't calling run()
32+
33+
build_app(app)
34+
```
35+
36+
::: view.build.build_app
37+
38+
## Build Steps
39+
40+
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](https://nextjs.org) 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.
41+
42+
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`:
43+
44+
```toml
45+
# view.toml
46+
[build.steps.nextjs]
47+
requires = ["npm"]
48+
command = "npm run build"
49+
```
50+
51+
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](https://view.zintensity.dev/building-projects/responses/#caching) the route.
52+
53+
For example:
54+
55+
```py
56+
from view import new_app
57+
58+
app = new_app()
59+
60+
@app.get("/", steps=["nextjs"], cache_rate=10000) # Reloads app every 10,000 requests
61+
async def index():
62+
return await app.template("out/index.html")
63+
64+
app.run()
65+
```
66+
67+
## Executing Build Scripts
68+
69+
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`:
70+
71+
```toml
72+
# view.toml
73+
[build.steps.foo]
74+
requires = []
75+
script = "foo.py"
76+
```
77+
78+
!!! note
79+
80+
`__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__"`
81+
82+
You can also specify a list of files or commands for both, to run multiple of either:
83+
84+
```toml
85+
# view.toml
86+
[build.steps.foo]
87+
requires = ["gcc"]
88+
script = ["foo.py", "bar.py"]
89+
command = ["gcc -c -Wall -Werror -fpic foo.c", "gcc -shared -o libfoo.so foo.o"]
90+
```
91+
92+
If the script needs to run asynchronous code, export a `__view_build__` from the script:
93+
94+
```py
95+
# build.py
96+
import aiofiles
97+
98+
# This function will be run by the view.py build system
99+
async def __view_build__():
100+
async with aiofiles.open("something.txt", "w") as f:
101+
await f.write("...")
102+
```
103+
104+
## Default Steps
105+
106+
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:
107+
108+
```toml
109+
# view.toml
110+
[build]
111+
default_steps = ["nextjs"]
112+
# Only NextJS will be built on startup
113+
114+
[build.steps.nextjs]
115+
requires = ["npm"]
116+
command = "npm run build"
117+
118+
[build.steps.php]
119+
requires = ["php"]
120+
command = "php -f payment.php"
121+
```
122+
123+
## Platform-Dependent Steps
124+
125+
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](https://toml.io/en/v1.0.0-rc.2#array-of-tables)) with `platform` values, view.py will run the entry based on the platform the app was run on.
126+
127+
For example, using the file reading example from above:
128+
129+
Notice the double brackets next to `[[build.steps.read_from_file]]`, specifying an array of tables.
130+
131+
```toml
132+
# view.toml
133+
134+
[[build.steps.read_from_file]]
135+
platform = ["mac", "linux"]
136+
command = "cat whatever.txt"
137+
138+
[[build.steps.read_from_file]]
139+
platform = "windows"
140+
command = "type whatever.txt"
141+
```
142+
143+
The `platform` value can be one of three things per entry:
144+
145+
- A list of platforms.
146+
- A string containing a single platform.
147+
- `None`, meaning to use this entry if no other platforms match.
148+
149+
For example, with a `None` platform set (on multiple entries), the above could be rewritten as:
150+
151+
```toml
152+
# view.toml
153+
154+
[[build.steps.read_from_file]]
155+
# Windows ONLY runs this step
156+
platform = "windows"
157+
command = "type whatever.txt"
158+
159+
[[build.steps.read_from_file]]
160+
# All other platforms run this!
161+
command = "cat whatever.txt"
162+
```
163+
164+
Note that only one step entry can have a `None` platform value, otherwise view.py will throw an error.
165+
166+
!!! note
167+
168+
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.
169+
170+
## Build Requirements
171+
172+
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.
173+
174+
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](https://github.com/ZeroIntensity/view.py/issues) requesting support for it as well.
175+
176+
### Custom Requirements
177+
178+
There are four types of custom requirements, which are specified by adding a prefix to the requirement name:
179+
180+
- Importing a Python module (`mod+`)
181+
- Executing a Python script (`script+`)
182+
- Checking if a path exists (`path+`)
183+
- Checking if a command exists (`command+`)
184+
185+
For example, the `command+gcc` would make sure that `gcc --version` return `0`:
186+
187+
```toml
188+
# view.toml
189+
[build.steps.c]
190+
requires = ["command+gcc"]
191+
command = "gcc *.c -o out"
192+
```
193+
194+
### The Requirement Protocol
195+
196+
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.
197+
198+
!!! note
199+
200+
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.
201+
202+
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:
203+
204+
```py
205+
# check_310.py
206+
import sys
207+
208+
async def __view_requirement__() -> bool:
209+
# Make sure we're running on at least Python 3.10
210+
return sys.version_info >= (3, 10)
211+
```
212+
213+
The above could actually be used via both `script+check_310.py` and `mod+check_310`.
214+
215+
!!! note
216+
217+
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](https://peps.python.org/pep-0723/) script metadata.
218+
219+
## Review
220+
221+
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](https://nextjs.org)). Builds can run commands, Python scripts, or both.
222+
223+
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.

0 commit comments

Comments
 (0)