koyoapp 0.1.0__tar.gz
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.
- koyoapp-0.1.0/MANIFEST.in +3 -0
- koyoapp-0.1.0/PKG-INFO +122 -0
- koyoapp-0.1.0/README.md +102 -0
- koyoapp-0.1.0/koyoapp/__init__.py +3 -0
- koyoapp-0.1.0/koyoapp/__main__.py +4 -0
- koyoapp-0.1.0/koyoapp/_placeholders.py +56 -0
- koyoapp-0.1.0/koyoapp/app.py +224 -0
- koyoapp-0.1.0/koyoapp/assets/apple-touch-icon.png +0 -0
- koyoapp-0.1.0/koyoapp/assets/favicon-96x96.png +0 -0
- koyoapp-0.1.0/koyoapp/assets/favicon.ico +0 -0
- koyoapp-0.1.0/koyoapp/assets/favicon.svg +1 -0
- koyoapp-0.1.0/koyoapp/assets/htmx.min.js +1 -0
- koyoapp-0.1.0/koyoapp/assets/icon.png +0 -0
- koyoapp-0.1.0/koyoapp/assets/logo.png +0 -0
- koyoapp-0.1.0/koyoapp/assets/site.webmanifest +21 -0
- koyoapp-0.1.0/koyoapp/assets/web-app-manifest-192x192.png +0 -0
- koyoapp-0.1.0/koyoapp/assets/web-app-manifest-512x512.png +0 -0
- koyoapp-0.1.0/koyoapp/build.py +82 -0
- koyoapp-0.1.0/koyoapp/cli.py +128 -0
- koyoapp-0.1.0/koyoapp/config.py +31 -0
- koyoapp-0.1.0/koyoapp/deps.py +312 -0
- koyoapp-0.1.0/koyoapp/dev.py +189 -0
- koyoapp-0.1.0/koyoapp/html.py +183 -0
- koyoapp-0.1.0/koyoapp/log.py +70 -0
- koyoapp-0.1.0/koyoapp/meta.py +51 -0
- koyoapp-0.1.0/koyoapp/net.py +35 -0
- koyoapp-0.1.0/koyoapp/reload.py +92 -0
- koyoapp-0.1.0/koyoapp/router.py +238 -0
- koyoapp-0.1.0/koyoapp/scaffold.py +60 -0
- koyoapp-0.1.0/koyoapp/serve.py +11 -0
- koyoapp-0.1.0/koyoapp/session.py +85 -0
- koyoapp-0.1.0/koyoapp/state.py +110 -0
- koyoapp-0.1.0/koyoapp/tailwind.py +86 -0
- koyoapp-0.1.0/koyoapp/templates.py +475 -0
- koyoapp-0.1.0/koyoapp/venv.py +17 -0
- koyoapp-0.1.0/koyoapp.egg-info/SOURCES.txt +43 -0
- koyoapp-0.1.0/pyproject.toml +40 -0
- koyoapp-0.1.0/setup.cfg +4 -0
- koyoapp-0.1.0/tests/test_build.py +47 -0
- koyoapp-0.1.0/tests/test_deps.py +249 -0
- koyoapp-0.1.0/tests/test_dev.py +83 -0
- koyoapp-0.1.0/tests/test_html.py +94 -0
- koyoapp-0.1.0/tests/test_install.py +151 -0
- koyoapp-0.1.0/tests/test_net.py +35 -0
- koyoapp-0.1.0/tests/test_router.py +374 -0
- koyoapp-0.1.0/tests/test_session.py +95 -0
koyoapp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: koyoapp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Koyo is a Python web framework that brings the Next.js app directory experience to pure Python.
|
|
5
|
+
Author: Koyo Developers
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: starlette>=0.37.0
|
|
10
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
11
|
+
Requires-Dist: watchfiles>=0.21.0
|
|
12
|
+
Requires-Dist: typer>=0.12.0
|
|
13
|
+
Requires-Dist: tomlkit>=0.12.0
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
16
|
+
Requires-Dist: httpx>=0.27; extra == "test"
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
19
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# Koyo
|
|
22
|
+
|
|
23
|
+
Koyo is a Python web framework that mirrors the developer experience of
|
|
24
|
+
Next.js (the app directory convention, file based routing, layouts,
|
|
25
|
+
reusable components) but is written entirely in Python and runs as a normal
|
|
26
|
+
Python web server. Since it is a normal Python process under the hood, any
|
|
27
|
+
package installed with pip is available immediately, with no build step or
|
|
28
|
+
compiler touching the Python code.
|
|
29
|
+
|
|
30
|
+
This is v1, frontend and routing only. No backend API layer, no database,
|
|
31
|
+
no auth, no deployment automation.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
Koyo requires Python 3.11 or newer.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
pip install koyoapp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
koyoapp create my-app
|
|
45
|
+
cd my-app
|
|
46
|
+
koyoapp dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Open http://127.0.0.1:2309. The home page renders out of the box with an
|
|
50
|
+
animated hero, a call to action linking to the Koyo documentation, and a
|
|
51
|
+
live session counter driven by htmx. Edit any file under `app/`, `public/`,
|
|
52
|
+
or `styles/` and the dev server restarts and the browser reloads
|
|
53
|
+
automatically.
|
|
54
|
+
|
|
55
|
+
## CLI
|
|
56
|
+
|
|
57
|
+
- `koyoapp create .` scaffolds a Koyo project into the current directory.
|
|
58
|
+
- `koyoapp create my-app` creates `my-app` and scaffolds inside it.
|
|
59
|
+
- `koyoapp dev` starts the dev server on port 2309 by default, override
|
|
60
|
+
with `--port`.
|
|
61
|
+
- `koyoapp build` prerenders every static route into `.koyo/build/site`.
|
|
62
|
+
|
|
63
|
+
## Project structure
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
my-app/
|
|
67
|
+
app/
|
|
68
|
+
layout.py
|
|
69
|
+
page.py
|
|
70
|
+
components/
|
|
71
|
+
counter.py
|
|
72
|
+
landing.py
|
|
73
|
+
site.py
|
|
74
|
+
public/
|
|
75
|
+
styles/
|
|
76
|
+
koyo.config.py
|
|
77
|
+
pyproject.toml
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Routing rules follow the Next.js app directory convention. Folders wrapped
|
|
81
|
+
in square brackets become dynamic path parameters. A page file must export
|
|
82
|
+
a function named `page`, and a layout file must export a function named
|
|
83
|
+
`layout(children)`. Layouts nest outward to inward, exactly like Next.js.
|
|
84
|
+
|
|
85
|
+
## Component system
|
|
86
|
+
|
|
87
|
+
`koyoapp.html` exposes function based HTML elements. Text content is HTML
|
|
88
|
+
escaped by default; use `Markup` (or `raw()`) for raw unescaped output.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from koyoapp.html import div, h1, p
|
|
92
|
+
|
|
93
|
+
def Card(title: str, body: str):
|
|
94
|
+
return div(class_="p-4 rounded-lg shadow bg-white")[
|
|
95
|
+
h1(class_="text-xl font-bold")[title],
|
|
96
|
+
p(class_="text-gray-600")[body],
|
|
97
|
+
]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Styles
|
|
101
|
+
|
|
102
|
+
`koyoapp dev` runs the Tailwind CLI in watch mode against the project and
|
|
103
|
+
compiles `styles/globals.css` to `styles/koyo.css`, which the root layout
|
|
104
|
+
links automatically. Any other `.css` file under `styles/` is served
|
|
105
|
+
untouched from `/styles/` and can be linked directly with a normal `link`
|
|
106
|
+
tag. The scaffold ships a light/dark theme switch: `public/theme.js`
|
|
107
|
+
applies the saved or system-preferred theme before first paint, the hero
|
|
108
|
+
button toggles it and stores the choice in `localStorage`, and dark mode
|
|
109
|
+
is class based (`darkMode: "class"`) so any element can use `dark:`
|
|
110
|
+
variants.
|
|
111
|
+
|
|
112
|
+
## Packages
|
|
113
|
+
|
|
114
|
+
There is no custom package manager. Activate the project virtualenv and use
|
|
115
|
+
plain pip:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
.venv/bin/pip install <package>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The package is importable in any `page.py`, `layout.py`, or component file
|
|
122
|
+
immediately, since Koyo runs as a normal Python process.
|
koyoapp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Koyo
|
|
2
|
+
|
|
3
|
+
Koyo is a Python web framework that mirrors the developer experience of
|
|
4
|
+
Next.js (the app directory convention, file based routing, layouts,
|
|
5
|
+
reusable components) but is written entirely in Python and runs as a normal
|
|
6
|
+
Python web server. Since it is a normal Python process under the hood, any
|
|
7
|
+
package installed with pip is available immediately, with no build step or
|
|
8
|
+
compiler touching the Python code.
|
|
9
|
+
|
|
10
|
+
This is v1, frontend and routing only. No backend API layer, no database,
|
|
11
|
+
no auth, no deployment automation.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Koyo requires Python 3.11 or newer.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
pip install koyoapp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
koyoapp create my-app
|
|
25
|
+
cd my-app
|
|
26
|
+
koyoapp dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Open http://127.0.0.1:2309. The home page renders out of the box with an
|
|
30
|
+
animated hero, a call to action linking to the Koyo documentation, and a
|
|
31
|
+
live session counter driven by htmx. Edit any file under `app/`, `public/`,
|
|
32
|
+
or `styles/` and the dev server restarts and the browser reloads
|
|
33
|
+
automatically.
|
|
34
|
+
|
|
35
|
+
## CLI
|
|
36
|
+
|
|
37
|
+
- `koyoapp create .` scaffolds a Koyo project into the current directory.
|
|
38
|
+
- `koyoapp create my-app` creates `my-app` and scaffolds inside it.
|
|
39
|
+
- `koyoapp dev` starts the dev server on port 2309 by default, override
|
|
40
|
+
with `--port`.
|
|
41
|
+
- `koyoapp build` prerenders every static route into `.koyo/build/site`.
|
|
42
|
+
|
|
43
|
+
## Project structure
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
my-app/
|
|
47
|
+
app/
|
|
48
|
+
layout.py
|
|
49
|
+
page.py
|
|
50
|
+
components/
|
|
51
|
+
counter.py
|
|
52
|
+
landing.py
|
|
53
|
+
site.py
|
|
54
|
+
public/
|
|
55
|
+
styles/
|
|
56
|
+
koyo.config.py
|
|
57
|
+
pyproject.toml
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Routing rules follow the Next.js app directory convention. Folders wrapped
|
|
61
|
+
in square brackets become dynamic path parameters. A page file must export
|
|
62
|
+
a function named `page`, and a layout file must export a function named
|
|
63
|
+
`layout(children)`. Layouts nest outward to inward, exactly like Next.js.
|
|
64
|
+
|
|
65
|
+
## Component system
|
|
66
|
+
|
|
67
|
+
`koyoapp.html` exposes function based HTML elements. Text content is HTML
|
|
68
|
+
escaped by default; use `Markup` (or `raw()`) for raw unescaped output.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from koyoapp.html import div, h1, p
|
|
72
|
+
|
|
73
|
+
def Card(title: str, body: str):
|
|
74
|
+
return div(class_="p-4 rounded-lg shadow bg-white")[
|
|
75
|
+
h1(class_="text-xl font-bold")[title],
|
|
76
|
+
p(class_="text-gray-600")[body],
|
|
77
|
+
]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Styles
|
|
81
|
+
|
|
82
|
+
`koyoapp dev` runs the Tailwind CLI in watch mode against the project and
|
|
83
|
+
compiles `styles/globals.css` to `styles/koyo.css`, which the root layout
|
|
84
|
+
links automatically. Any other `.css` file under `styles/` is served
|
|
85
|
+
untouched from `/styles/` and can be linked directly with a normal `link`
|
|
86
|
+
tag. The scaffold ships a light/dark theme switch: `public/theme.js`
|
|
87
|
+
applies the saved or system-preferred theme before first paint, the hero
|
|
88
|
+
button toggles it and stores the choice in `localStorage`, and dark mode
|
|
89
|
+
is class based (`darkMode: "class"`) so any element can use `dark:`
|
|
90
|
+
variants.
|
|
91
|
+
|
|
92
|
+
## Packages
|
|
93
|
+
|
|
94
|
+
There is no custom package manager. Activate the project virtualenv and use
|
|
95
|
+
plain pip:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
.venv/bin/pip install <package>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The package is importable in any `page.py`, `layout.py`, or component file
|
|
102
|
+
immediately, since Koyo runs as a normal Python process.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Generate simple placeholder PNG and ICO files with no dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
import zlib
|
|
7
|
+
|
|
8
|
+
LOGO_RGB = (79, 70, 229)
|
|
9
|
+
ICON_RGB = (15, 118, 110)
|
|
10
|
+
FAVICON_RGB = (30, 27, 75)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _png_chunk(kind: bytes, data: bytes) -> bytes:
|
|
14
|
+
block = kind + data
|
|
15
|
+
crc = zlib.crc32(block) & 0xFFFFFFFF
|
|
16
|
+
return struct.pack(">I", len(data)) + block + struct.pack(">I", crc)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_png(width: int, height: int, rgb: tuple[int, int, int]) -> bytes:
|
|
20
|
+
ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
|
|
21
|
+
row = bytes(rgb) * width
|
|
22
|
+
scanlines = b"".join(b"\x00" + row for _ in range(height))
|
|
23
|
+
idat = zlib.compress(scanlines, 9)
|
|
24
|
+
return (
|
|
25
|
+
b"\x89PNG\r\n\x1a\n"
|
|
26
|
+
+ _png_chunk(b"IHDR", ihdr)
|
|
27
|
+
+ _png_chunk(b"IDAT", idat)
|
|
28
|
+
+ _png_chunk(b"IEND", b"")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_ico(entries: list[tuple[int, bytes]]) -> bytes:
|
|
33
|
+
count = len(entries)
|
|
34
|
+
header = struct.pack("<HHH", 0, 1, count)
|
|
35
|
+
offset = 6 + 16 * count
|
|
36
|
+
directory = b""
|
|
37
|
+
payload = b""
|
|
38
|
+
for size, png in entries:
|
|
39
|
+
width_byte = size if size < 256 else 0
|
|
40
|
+
directory += struct.pack(
|
|
41
|
+
"<BBBBHHII", width_byte, width_byte, 0, 0, 1, 32, len(png), offset
|
|
42
|
+
)
|
|
43
|
+
payload += png
|
|
44
|
+
offset += len(png)
|
|
45
|
+
return header + directory + payload
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def make_favicon_ico() -> bytes:
|
|
49
|
+
return make_ico(
|
|
50
|
+
[
|
|
51
|
+
(16, make_png(16, 16, FAVICON_RGB)),
|
|
52
|
+
(32, make_png(32, 32, FAVICON_RGB)),
|
|
53
|
+
(48, make_png(48, 48, FAVICON_RGB)),
|
|
54
|
+
(256, make_png(256, 256, FAVICON_RGB)),
|
|
55
|
+
]
|
|
56
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Starlette application construction for a Koyo project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import traceback
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from starlette.applications import Starlette
|
|
10
|
+
from starlette.responses import HTMLResponse, Response
|
|
11
|
+
from starlette.routing import Mount, Route
|
|
12
|
+
from starlette.staticfiles import StaticFiles
|
|
13
|
+
|
|
14
|
+
from .html import Markup, a, body, div, h1, head, html, meta, p, pre, style, title
|
|
15
|
+
from .reload import current_token, dev_mode, inject_dev_reload
|
|
16
|
+
from .router import (
|
|
17
|
+
RouteEntry,
|
|
18
|
+
RouteError,
|
|
19
|
+
module_has_fragment,
|
|
20
|
+
module_has_page,
|
|
21
|
+
render_fragment,
|
|
22
|
+
render_route,
|
|
23
|
+
reset_project_modules,
|
|
24
|
+
scan_app_dir,
|
|
25
|
+
)
|
|
26
|
+
from .session import SessionMiddleware
|
|
27
|
+
from .state import STATE_URL_PREFIX, render_state_action
|
|
28
|
+
|
|
29
|
+
_ERROR_CSS = """\
|
|
30
|
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
31
|
+
background: #f1f5f9; color: #0f172a; }
|
|
32
|
+
.container { display: flex; min-height: 100vh; align-items: center; justify-content: center; padding: 1.5rem; }
|
|
33
|
+
.card { max-width: 40rem; width: 100%; background: #ffffff; border: 1px solid #e2e8f0;
|
|
34
|
+
border-radius: 12px; padding: 2rem; box-shadow: 0 4px 12px rgba(15, 23, 42, 0.06); }
|
|
35
|
+
h1 { font-size: 1.5rem; margin: 0 0 0.5rem; }
|
|
36
|
+
p { color: #475569; line-height: 1.6; }
|
|
37
|
+
a { color: #4f46e5; }
|
|
38
|
+
.eyebrow { font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: #ef4444;
|
|
39
|
+
font-weight: 600; margin: 0 0 0.25rem; }
|
|
40
|
+
.message { margin: 0; }
|
|
41
|
+
.where { margin-top: 1rem; color: #b45309; font-family: ui-monospace, monospace;
|
|
42
|
+
background: #fef3c7; border-radius: 8px; padding: 0.5rem 0.75rem; }
|
|
43
|
+
pre.trace { overflow-x: auto; background: #0f172a; color: #e2e8f0; padding: 1rem;
|
|
44
|
+
border-radius: 8px; font-size: 0.8rem; line-height: 1.5; }
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _error_page(status: int, heading: str, message: str, detail: str = "") -> str:
|
|
49
|
+
children = [h1()[heading], p()[message]]
|
|
50
|
+
if detail:
|
|
51
|
+
children.append(pre(class_="trace")[detail])
|
|
52
|
+
children.append(p(class_="home")[a(href="/")["Back to home"]])
|
|
53
|
+
page = html(lang="en")[
|
|
54
|
+
head()[
|
|
55
|
+
meta(charset="utf-8"),
|
|
56
|
+
title()[f"{status} {heading}"],
|
|
57
|
+
style()[Markup(_ERROR_CSS)],
|
|
58
|
+
],
|
|
59
|
+
body()[
|
|
60
|
+
div(class_="container")[
|
|
61
|
+
div(class_="card")[children]
|
|
62
|
+
]
|
|
63
|
+
],
|
|
64
|
+
]
|
|
65
|
+
return "<!doctype html>\n" + str(page)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _dev_error_page(exc: BaseException, route_path: str) -> str:
|
|
69
|
+
tb = getattr(exc, "__traceback__", None)
|
|
70
|
+
if tb is not None:
|
|
71
|
+
trace_text = "".join(traceback.format_exception(type(exc), exc, tb))
|
|
72
|
+
else:
|
|
73
|
+
trace_text = f"{type(exc).__name__}: {exc}"
|
|
74
|
+
frames = traceback.extract_tb(tb) if tb is not None else []
|
|
75
|
+
location = ""
|
|
76
|
+
if frames:
|
|
77
|
+
frame = frames[-1]
|
|
78
|
+
location = f"{frame.filename}:{frame.lineno} in {frame.name}"
|
|
79
|
+
heading = type(exc).__name__
|
|
80
|
+
message = str(exc) or heading
|
|
81
|
+
children = [
|
|
82
|
+
p(class_="eyebrow")[f"Error in {route_path}"],
|
|
83
|
+
h1()[heading],
|
|
84
|
+
p(class_="message")[message],
|
|
85
|
+
]
|
|
86
|
+
if location:
|
|
87
|
+
children.append(p(class_="where")[location])
|
|
88
|
+
children.append(pre(class_="trace")[trace_text])
|
|
89
|
+
children.append(p(class_="home")[a(href="/")["Back to home"]])
|
|
90
|
+
page = html(lang="en")[
|
|
91
|
+
head()[
|
|
92
|
+
meta(charset="utf-8"),
|
|
93
|
+
title()[f"{heading} on {route_path}"],
|
|
94
|
+
style()[Markup(_ERROR_CSS)],
|
|
95
|
+
],
|
|
96
|
+
body()[
|
|
97
|
+
div(class_="container")[
|
|
98
|
+
div(class_="card")[children]
|
|
99
|
+
]
|
|
100
|
+
],
|
|
101
|
+
]
|
|
102
|
+
return "<!doctype html>\n" + str(page)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _html_response(content: str, status_code: int = 200) -> HTMLResponse:
|
|
106
|
+
return HTMLResponse(inject_dev_reload(content), status_code=status_code)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def _not_found(request, exc) -> HTMLResponse:
|
|
110
|
+
message = f"There is no route or public file for {request.url.path}."
|
|
111
|
+
return _html_response(_error_page(404, "Not Found", message), 404)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _request_wants_fragment(entry: RouteEntry, request) -> bool:
|
|
115
|
+
if not module_has_fragment(entry):
|
|
116
|
+
return False
|
|
117
|
+
if not module_has_page(entry):
|
|
118
|
+
return True
|
|
119
|
+
return (request.headers.get("hx-request") or "").lower() == "true"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _make_endpoint(entry: RouteEntry):
|
|
123
|
+
async def endpoint(request):
|
|
124
|
+
status = 200
|
|
125
|
+
try:
|
|
126
|
+
params = dict(request.path_params)
|
|
127
|
+
if _request_wants_fragment(entry, request):
|
|
128
|
+
return HTMLResponse(render_fragment(entry, params, request))
|
|
129
|
+
return _html_response(render_route(entry, params, request))
|
|
130
|
+
except RouteError as exc:
|
|
131
|
+
if dev_mode():
|
|
132
|
+
content = _dev_error_page(exc, entry.path)
|
|
133
|
+
status = 500
|
|
134
|
+
else:
|
|
135
|
+
content = _error_page(
|
|
136
|
+
500, "Internal Server Error", f"Rendering {entry.path} failed."
|
|
137
|
+
)
|
|
138
|
+
status = 500
|
|
139
|
+
except Exception:
|
|
140
|
+
if dev_mode():
|
|
141
|
+
content = _dev_error_page(sys.exc_info()[1], entry.path)
|
|
142
|
+
else:
|
|
143
|
+
content = _error_page(
|
|
144
|
+
500,
|
|
145
|
+
"Internal Server Error",
|
|
146
|
+
f"Rendering {entry.path} raised an unexpected error.",
|
|
147
|
+
)
|
|
148
|
+
status = 500
|
|
149
|
+
return _html_response(content, status_code=status)
|
|
150
|
+
|
|
151
|
+
return endpoint
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def _state_endpoint(request):
|
|
155
|
+
token = request.path_params["token"]
|
|
156
|
+
try:
|
|
157
|
+
content, status = render_state_action(request, token)
|
|
158
|
+
return HTMLResponse(content, status_code=status)
|
|
159
|
+
except Exception:
|
|
160
|
+
if dev_mode():
|
|
161
|
+
content = _dev_error_page(sys.exc_info()[1], f"{STATE_URL_PREFIX}/{token}")
|
|
162
|
+
status = 500
|
|
163
|
+
else:
|
|
164
|
+
content = _error_page(
|
|
165
|
+
500,
|
|
166
|
+
"Internal Server Error",
|
|
167
|
+
"The session state update failed.",
|
|
168
|
+
)
|
|
169
|
+
status = 500
|
|
170
|
+
return HTMLResponse(content, status_code=status)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def _reload_probe(request) -> Response:
|
|
174
|
+
project_dir = request.app.state.koyo_project_dir
|
|
175
|
+
token = current_token(project_dir)
|
|
176
|
+
since = request.query_params.get("since", "")
|
|
177
|
+
headers = {"Cache-Control": "no-store", "X-Koyo-Token": token}
|
|
178
|
+
if not since or since == token:
|
|
179
|
+
return Response(status_code=204, headers=headers)
|
|
180
|
+
return Response(token, status_code=200, headers=headers)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def build_app(project_dir: str | Path) -> Starlette:
|
|
184
|
+
project_dir = Path(project_dir).resolve()
|
|
185
|
+
root = str(project_dir)
|
|
186
|
+
if sys.path and sys.path[0] != root:
|
|
187
|
+
sys.path.insert(0, root)
|
|
188
|
+
reset_project_modules()
|
|
189
|
+
|
|
190
|
+
app_dir = project_dir / "app"
|
|
191
|
+
public_dir = project_dir / "public"
|
|
192
|
+
styles_dir = project_dir / "styles"
|
|
193
|
+
|
|
194
|
+
public_dir.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
styles_dir.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
|
|
197
|
+
entries = scan_app_dir(app_dir)
|
|
198
|
+
|
|
199
|
+
routes = [
|
|
200
|
+
Route(
|
|
201
|
+
f"{STATE_URL_PREFIX}/{{token}}",
|
|
202
|
+
_state_endpoint,
|
|
203
|
+
methods=["GET", "HEAD", "POST"],
|
|
204
|
+
)
|
|
205
|
+
]
|
|
206
|
+
if dev_mode():
|
|
207
|
+
routes.append(Route("/__koyo-reload", _reload_probe))
|
|
208
|
+
for entry in entries:
|
|
209
|
+
routes.append(
|
|
210
|
+
Route(
|
|
211
|
+
entry.path,
|
|
212
|
+
_make_endpoint(entry),
|
|
213
|
+
methods=["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
if styles_dir.is_dir():
|
|
217
|
+
routes.append(Mount("/styles", StaticFiles(directory=str(styles_dir))))
|
|
218
|
+
if public_dir.is_dir():
|
|
219
|
+
routes.append(Mount("/", StaticFiles(directory=str(public_dir))))
|
|
220
|
+
|
|
221
|
+
application = Starlette(routes=routes, exception_handlers={404: _not_found})
|
|
222
|
+
application.add_middleware(SessionMiddleware)
|
|
223
|
+
application.state.koyo_project_dir = str(project_dir)
|
|
224
|
+
return application
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><metadata><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description><dc:creator>RealFaviconGenerator</dc:creator><dc:source>https://realfavicongenerator.net</dc:source></rdf:Description></rdf:RDF></metadata><g clip-path="url(#SvgjsClipPath1125)"><rect width="1000" height="1000" fill="#1800ad"></rect><g transform="matrix(25,0,0,25,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="40" height="40" viewBox="0 0 40 40"><image width="40" height="40" xlink:href="data:image/png;base64,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"></image></svg></g></g><defs><clipPath id="SvgjsClipPath1125"><rect width="1000" height="1000" x="0" y="0" rx="500" ry="500"></rect></clipPath></defs></svg>
|