z8ter 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.
- z8ter-0.1.0/LICENSE +21 -0
- z8ter-0.1.0/PKG-INFO +162 -0
- z8ter-0.1.0/README.md +130 -0
- z8ter-0.1.0/api/__init__.py +0 -0
- z8ter-0.1.0/api/hello.py +13 -0
- z8ter-0.1.0/pyproject.toml +3 -0
- z8ter-0.1.0/setup.cfg +4 -0
- z8ter-0.1.0/setup.py +37 -0
- z8ter-0.1.0/views/__init__.py +0 -0
- z8ter-0.1.0/views/about.py +8 -0
- z8ter-0.1.0/views/index.py +9 -0
- z8ter-0.1.0/z8ter/__init__.py +14 -0
- z8ter-0.1.0/z8ter/api.py +41 -0
- z8ter-0.1.0/z8ter/cli.py +120 -0
- z8ter-0.1.0/z8ter/config.py +10 -0
- z8ter-0.1.0/z8ter/core.py +102 -0
- z8ter-0.1.0/z8ter/page.py +29 -0
- z8ter-0.1.0/z8ter/router.py +95 -0
- z8ter-0.1.0/z8ter.egg-info/PKG-INFO +162 -0
- z8ter-0.1.0/z8ter.egg-info/SOURCES.txt +22 -0
- z8ter-0.1.0/z8ter.egg-info/dependency_links.txt +1 -0
- z8ter-0.1.0/z8ter.egg-info/entry_points.txt +2 -0
- z8ter-0.1.0/z8ter.egg-info/requires.txt +2 -0
- z8ter-0.1.0/z8ter.egg-info/top_level.txt +3 -0
z8ter-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ashesh Nepal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
z8ter-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: z8ter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SSR-first Python web framework with file-based views, tinyCSR islands, and decorator-driven APIs (Starlette + Jinja2).
|
|
5
|
+
Home-page: https://github.com/ashesh808/Z8ter
|
|
6
|
+
Author: Ashesh Nepal
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: starlette<1.0,>=0.47
|
|
21
|
+
Requires-Dist: Jinja2<4.0,>=3.1
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: license
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
Dynamic: requires-dist
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# Z8ter.py
|
|
34
|
+
|
|
35
|
+
**Z8ter** is a lightweight, Laravel-inspired full-stack Python web framework built on [Starlette], designed for rapid development with tight integration between backend logic and frontend templates—plus small client-side “islands” where they make sense.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ✨ Features (Current)
|
|
40
|
+
|
|
41
|
+
### 1) File-Based Views (SSR)
|
|
42
|
+
- Files under `views/` become routes automatically.
|
|
43
|
+
- Each view pairs Python logic with a Jinja template in `templates/`.
|
|
44
|
+
- A stable `page_id` (derived from `views/` path) is injected into templates and used by the frontend loader to hydrate per-page JS.
|
|
45
|
+
|
|
46
|
+
### 2) Jinja2 Templating
|
|
47
|
+
- Template inheritance with `{% extends %}` / `{% block %}`.
|
|
48
|
+
- Templates live in `templates/` (default extension: `.jinja`).
|
|
49
|
+
|
|
50
|
+
### 3) Small CSR “Islands”
|
|
51
|
+
- A tiny client router lazy-loads `/static/js/pages/<page_id>.js` and runs its default export.
|
|
52
|
+
- Great for interactive bits (theme toggles, pings, clipboard, etc.) without going full SPA.
|
|
53
|
+
|
|
54
|
+
### 4) Decorator-Driven APIs
|
|
55
|
+
- Classes under `api/` subclass `API` and register endpoints with a decorator.
|
|
56
|
+
- Each class mounts under `/api/<id>` (derived from module path).
|
|
57
|
+
|
|
58
|
+
> Example shape (conceptual):
|
|
59
|
+
> ```
|
|
60
|
+
> api/hello.py → /api/hello
|
|
61
|
+
> views/about.py → /about
|
|
62
|
+
> templates/about.jinja + static/js/pages/about.js (island)
|
|
63
|
+
> ```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🚀 Getting Started
|
|
68
|
+
|
|
69
|
+
### Prerequisites
|
|
70
|
+
- Python 3.11+ and `pip`
|
|
71
|
+
- Node 18+ and `npm`
|
|
72
|
+
|
|
73
|
+
### Install & Run (dev)
|
|
74
|
+
```bash
|
|
75
|
+
# 1) Python deps (in a venv)
|
|
76
|
+
python -m venv .venv
|
|
77
|
+
source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1
|
|
78
|
+
pip install -r requirements.txt # or: pip install -e .
|
|
79
|
+
|
|
80
|
+
# 2) Frontend deps
|
|
81
|
+
npm install
|
|
82
|
+
|
|
83
|
+
# 3) Dev server(s)
|
|
84
|
+
npm run dev
|
|
85
|
+
````
|
|
86
|
+
|
|
87
|
+
> `npm run dev` runs the dev workflow (backend + assets). Check the terminal for the local URL.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 📁 Project Structure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
.
|
|
95
|
+
├─ api/ # API classes (@API.endpoint)
|
|
96
|
+
│ └─ hello.py
|
|
97
|
+
├─ views/ # File-based pages (SSR)
|
|
98
|
+
│ └─ index.py
|
|
99
|
+
├─ templates/ # Jinja templates
|
|
100
|
+
│ ├─ base.jinja
|
|
101
|
+
│ └─ index.jinja
|
|
102
|
+
├─ static/
|
|
103
|
+
│ └─ js/
|
|
104
|
+
│ └─ pages/ # Per-page islands: about.js, app/home.js, ...
|
|
105
|
+
│ └─ common.js
|
|
106
|
+
├─ z8ter/ # Framework core (Page, API, router)
|
|
107
|
+
└─ main.py # App entrypoint
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🧩 Usage Examples
|
|
113
|
+
|
|
114
|
+
### View + Template (SSR)
|
|
115
|
+
|
|
116
|
+
```jinja
|
|
117
|
+
{# templates/index.jinja #}
|
|
118
|
+
{% extends "base.jinja" %}
|
|
119
|
+
{% block content %}
|
|
120
|
+
<h1>{{ title }}</h1>
|
|
121
|
+
<div id="api-response"></div>
|
|
122
|
+
{% endblock %}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Client Island (runs when `page_id` matches)
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// static/js/pages/common.ts (or a specific page module)
|
|
129
|
+
export default async function init() {
|
|
130
|
+
// hydrate interactive bits, fetch data, etc.
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Minimal API Class
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# api/hello.py
|
|
138
|
+
from z8ter.api import API
|
|
139
|
+
|
|
140
|
+
class Hello(API):
|
|
141
|
+
@API.endpoint("GET", "/hello")
|
|
142
|
+
async def hello(self, request):
|
|
143
|
+
return {"ok": True, "message": "Hello from Z8ter"}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 🛣️ Planned
|
|
149
|
+
|
|
150
|
+
* **CLI scaffolding**: `z8 new`, `z8 dev`, `z8 create_page <name>`
|
|
151
|
+
* **Auth scaffolding**: login/register/logout + session helpers
|
|
152
|
+
* **Stripe integration**: pricing page, checkout routes, webhooks
|
|
153
|
+
* **DB adapters**: SQLite default, Postgres option
|
|
154
|
+
* **HTMX + Tailwind/DaisyUI** polish out of the box
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 🧠 Philosophy
|
|
159
|
+
|
|
160
|
+
* Conventions over configuration
|
|
161
|
+
* SSR-first with tiny CSR islands
|
|
162
|
+
* Small surface area; sharp, pragmatic tools
|
z8ter-0.1.0/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Z8ter.py
|
|
2
|
+
|
|
3
|
+
**Z8ter** is a lightweight, Laravel-inspired full-stack Python web framework built on [Starlette], designed for rapid development with tight integration between backend logic and frontend templates—plus small client-side “islands” where they make sense.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ✨ Features (Current)
|
|
8
|
+
|
|
9
|
+
### 1) File-Based Views (SSR)
|
|
10
|
+
- Files under `views/` become routes automatically.
|
|
11
|
+
- Each view pairs Python logic with a Jinja template in `templates/`.
|
|
12
|
+
- A stable `page_id` (derived from `views/` path) is injected into templates and used by the frontend loader to hydrate per-page JS.
|
|
13
|
+
|
|
14
|
+
### 2) Jinja2 Templating
|
|
15
|
+
- Template inheritance with `{% extends %}` / `{% block %}`.
|
|
16
|
+
- Templates live in `templates/` (default extension: `.jinja`).
|
|
17
|
+
|
|
18
|
+
### 3) Small CSR “Islands”
|
|
19
|
+
- A tiny client router lazy-loads `/static/js/pages/<page_id>.js` and runs its default export.
|
|
20
|
+
- Great for interactive bits (theme toggles, pings, clipboard, etc.) without going full SPA.
|
|
21
|
+
|
|
22
|
+
### 4) Decorator-Driven APIs
|
|
23
|
+
- Classes under `api/` subclass `API` and register endpoints with a decorator.
|
|
24
|
+
- Each class mounts under `/api/<id>` (derived from module path).
|
|
25
|
+
|
|
26
|
+
> Example shape (conceptual):
|
|
27
|
+
> ```
|
|
28
|
+
> api/hello.py → /api/hello
|
|
29
|
+
> views/about.py → /about
|
|
30
|
+
> templates/about.jinja + static/js/pages/about.js (island)
|
|
31
|
+
> ```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🚀 Getting Started
|
|
36
|
+
|
|
37
|
+
### Prerequisites
|
|
38
|
+
- Python 3.11+ and `pip`
|
|
39
|
+
- Node 18+ and `npm`
|
|
40
|
+
|
|
41
|
+
### Install & Run (dev)
|
|
42
|
+
```bash
|
|
43
|
+
# 1) Python deps (in a venv)
|
|
44
|
+
python -m venv .venv
|
|
45
|
+
source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1
|
|
46
|
+
pip install -r requirements.txt # or: pip install -e .
|
|
47
|
+
|
|
48
|
+
# 2) Frontend deps
|
|
49
|
+
npm install
|
|
50
|
+
|
|
51
|
+
# 3) Dev server(s)
|
|
52
|
+
npm run dev
|
|
53
|
+
````
|
|
54
|
+
|
|
55
|
+
> `npm run dev` runs the dev workflow (backend + assets). Check the terminal for the local URL.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 📁 Project Structure
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
.
|
|
63
|
+
├─ api/ # API classes (@API.endpoint)
|
|
64
|
+
│ └─ hello.py
|
|
65
|
+
├─ views/ # File-based pages (SSR)
|
|
66
|
+
│ └─ index.py
|
|
67
|
+
├─ templates/ # Jinja templates
|
|
68
|
+
│ ├─ base.jinja
|
|
69
|
+
│ └─ index.jinja
|
|
70
|
+
├─ static/
|
|
71
|
+
│ └─ js/
|
|
72
|
+
│ └─ pages/ # Per-page islands: about.js, app/home.js, ...
|
|
73
|
+
│ └─ common.js
|
|
74
|
+
├─ z8ter/ # Framework core (Page, API, router)
|
|
75
|
+
└─ main.py # App entrypoint
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🧩 Usage Examples
|
|
81
|
+
|
|
82
|
+
### View + Template (SSR)
|
|
83
|
+
|
|
84
|
+
```jinja
|
|
85
|
+
{# templates/index.jinja #}
|
|
86
|
+
{% extends "base.jinja" %}
|
|
87
|
+
{% block content %}
|
|
88
|
+
<h1>{{ title }}</h1>
|
|
89
|
+
<div id="api-response"></div>
|
|
90
|
+
{% endblock %}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Client Island (runs when `page_id` matches)
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// static/js/pages/common.ts (or a specific page module)
|
|
97
|
+
export default async function init() {
|
|
98
|
+
// hydrate interactive bits, fetch data, etc.
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Minimal API Class
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# api/hello.py
|
|
106
|
+
from z8ter.api import API
|
|
107
|
+
|
|
108
|
+
class Hello(API):
|
|
109
|
+
@API.endpoint("GET", "/hello")
|
|
110
|
+
async def hello(self, request):
|
|
111
|
+
return {"ok": True, "message": "Hello from Z8ter"}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 🛣️ Planned
|
|
117
|
+
|
|
118
|
+
* **CLI scaffolding**: `z8 new`, `z8 dev`, `z8 create_page <name>`
|
|
119
|
+
* **Auth scaffolding**: login/register/logout + session helpers
|
|
120
|
+
* **Stripe integration**: pricing page, checkout routes, webhooks
|
|
121
|
+
* **DB adapters**: SQLite default, Postgres option
|
|
122
|
+
* **HTMX + Tailwind/DaisyUI** polish out of the box
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 🧠 Philosophy
|
|
127
|
+
|
|
128
|
+
* Conventions over configuration
|
|
129
|
+
* SSR-first with tiny CSR islands
|
|
130
|
+
* Small surface area; sharp, pragmatic tools
|
|
File without changes
|
z8ter-0.1.0/api/hello.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from z8ter.api import API
|
|
2
|
+
from starlette.responses import JSONResponse
|
|
3
|
+
from starlette.requests import Request
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Hello(API):
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
super().__init__()
|
|
9
|
+
|
|
10
|
+
@API.endpoint("GET", "/")
|
|
11
|
+
async def send_hello(self, request: Request) -> JSONResponse:
|
|
12
|
+
content = {"message": "Hello from the API!"}
|
|
13
|
+
return JSONResponse(content, 200)
|
z8ter-0.1.0/setup.cfg
ADDED
z8ter-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from setuptools import setup, find_packages
|
|
3
|
+
|
|
4
|
+
README = (Path(__file__).parent / "README.md").read_text(encoding="utf-8")
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="z8ter",
|
|
8
|
+
version="0.1.0",
|
|
9
|
+
description="SSR-first Python web framework with file-based views, tiny"
|
|
10
|
+
"CSR islands, and decorator-driven APIs (Starlette + Jinja2).",
|
|
11
|
+
long_description=README,
|
|
12
|
+
long_description_content_type="text/markdown",
|
|
13
|
+
url="https://github.com/ashesh808/Z8ter",
|
|
14
|
+
author="Ashesh Nepal",
|
|
15
|
+
license="MIT",
|
|
16
|
+
packages=find_packages(exclude=(
|
|
17
|
+
"tests", "tests.*", "examples", "examples.*"
|
|
18
|
+
)),
|
|
19
|
+
include_package_data=True,
|
|
20
|
+
python_requires=">=3.11",
|
|
21
|
+
install_requires=[
|
|
22
|
+
"starlette>=0.47,<1.0",
|
|
23
|
+
"Jinja2>=3.1,<4.0",
|
|
24
|
+
],
|
|
25
|
+
entry_points={"console_scripts": ["z8=z8ter.cli:main"]},
|
|
26
|
+
classifiers=[
|
|
27
|
+
"Development Status :: 3 - Alpha",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Framework :: AsyncIO",
|
|
35
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
36
|
+
],
|
|
37
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from z8ter.page import Page
|
|
2
|
+
from starlette.requests import Request
|
|
3
|
+
from starlette.responses import Response
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Index(Page):
|
|
7
|
+
async def get(self, request: Request) -> Response:
|
|
8
|
+
data = {"title": "Welcome to Z8ter!"}
|
|
9
|
+
return self.render(request, "index.jinja", data)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
__all__ = ["API", "Page"]
|
|
2
|
+
__version__ = "0.1.0"
|
|
3
|
+
from .api import API
|
|
4
|
+
from .page import Page
|
|
5
|
+
from starlette.templating import Jinja2Templates
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
9
|
+
FAVICON_PATH = BASE_DIR / "static" / "favicon" / "favicon.ico"
|
|
10
|
+
TEMPLATES_DIR = BASE_DIR / "templates"
|
|
11
|
+
VIEWS_DIR = BASE_DIR / "views"
|
|
12
|
+
TS_DIR = BASE_DIR / "src" / "ts"
|
|
13
|
+
API_DIR = BASE_DIR / "api"
|
|
14
|
+
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
z8ter-0.1.0/z8ter/api.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from starlette.routing import Route, Mount
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class API():
|
|
6
|
+
"""
|
|
7
|
+
Class which supports endpoint decorators to provide a list of endpoints
|
|
8
|
+
for a particular app
|
|
9
|
+
"""
|
|
10
|
+
def __init_subclass__(cls, **kwargs):
|
|
11
|
+
super().__init_subclass__(**kwargs)
|
|
12
|
+
mod = cls.__module__
|
|
13
|
+
if mod.startswith("api."):
|
|
14
|
+
id = mod.removeprefix("api.")
|
|
15
|
+
else:
|
|
16
|
+
id = mod
|
|
17
|
+
cls._api_id = id.replace('.', '/')
|
|
18
|
+
cls._endpoints = []
|
|
19
|
+
cls._endpoints = []
|
|
20
|
+
for name, obj in cls.__dict__.items():
|
|
21
|
+
meta = getattr(obj, "_z8_endpoint", None)
|
|
22
|
+
if meta:
|
|
23
|
+
http_method, subpath = meta
|
|
24
|
+
cls._endpoints.append((http_method, subpath, name))
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def build_mount(cls) -> Mount:
|
|
28
|
+
prefix = f"/api/{getattr(cls, "_api_id")}"
|
|
29
|
+
inst = cls()
|
|
30
|
+
routes = [
|
|
31
|
+
Route(subpath, endpoint=getattr(inst, func_name), methods=[method])
|
|
32
|
+
for (method, subpath, func_name) in getattr(cls, "_endpoints", [])
|
|
33
|
+
]
|
|
34
|
+
return Mount(prefix, routes=routes)
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def endpoint(method: str, path: str):
|
|
38
|
+
def deco(fn):
|
|
39
|
+
setattr(fn, "_z8_endpoint", (method.upper(), path))
|
|
40
|
+
return fn
|
|
41
|
+
return deco
|
z8ter-0.1.0/z8ter/cli.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from z8ter import TEMPLATES_DIR, VIEWS_DIR, TS_DIR, API_DIR
|
|
2
|
+
import argparse
|
|
3
|
+
|
|
4
|
+
'''
|
|
5
|
+
CLI support for the following commands -
|
|
6
|
+
1. z8ter create_page newpage
|
|
7
|
+
2. z8ter new project
|
|
8
|
+
3. z8ter run
|
|
9
|
+
4. z8ter run dev
|
|
10
|
+
'''
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def set_page_content(page_name_lower, class_name) -> dict:
|
|
14
|
+
content = {}
|
|
15
|
+
content["template_content"] = f"""{{% extends "components/base.jinja" %}}
|
|
16
|
+
{{% block content %}}
|
|
17
|
+
<h1>{class_name}</h1>
|
|
18
|
+
{{% endblock %}}
|
|
19
|
+
"""
|
|
20
|
+
content["view_content"] = f"""from z8ter.page import Page
|
|
21
|
+
from starlette.requests import Request
|
|
22
|
+
from starlette.responses import Response
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class {class_name}(Page):
|
|
26
|
+
async def get(self, request: Request) -> Response:
|
|
27
|
+
return self.render(request, "{page_name_lower}.jinja", {{}})
|
|
28
|
+
"""
|
|
29
|
+
content["ts_content"] = (
|
|
30
|
+
f"export default function init{class_name}(): void {{}}"
|
|
31
|
+
)
|
|
32
|
+
return content
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create_page(page_name: str):
|
|
36
|
+
page_name_lower = page_name.lower()
|
|
37
|
+
class_name = page_name.capitalize()
|
|
38
|
+
template_path = TEMPLATES_DIR / f"{page_name_lower}.jinja"
|
|
39
|
+
view_path = VIEWS_DIR / f"{page_name_lower}.py"
|
|
40
|
+
ts_path = TS_DIR / "pages" /f"{page_name_lower}.ts"
|
|
41
|
+
TEMPLATES_DIR.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
VIEWS_DIR.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
TS_DIR.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
content = set_page_content(page_name_lower, class_name)
|
|
45
|
+
if not template_path.exists():
|
|
46
|
+
template_path.write_text(content["template_content"], encoding="utf-8")
|
|
47
|
+
print(f"Created template: {template_path}")
|
|
48
|
+
else:
|
|
49
|
+
print(f"Template already exists: {template_path}")
|
|
50
|
+
if not view_path.exists():
|
|
51
|
+
view_path.write_text(content["view_content"], encoding="utf-8")
|
|
52
|
+
print(f"Created view: {view_path}")
|
|
53
|
+
else:
|
|
54
|
+
print(f"View already exists: {view_path}")
|
|
55
|
+
if not ts_path.exists():
|
|
56
|
+
ts_path.write_text(content["ts_content"], encoding="utf-8")
|
|
57
|
+
print(f"Created script: {ts_path}")
|
|
58
|
+
else:
|
|
59
|
+
print(f"Script already exists: {ts_path}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_api(api_name: str):
|
|
63
|
+
api_name_lower = api_name.lower()
|
|
64
|
+
class_name = api_name.capitalize()
|
|
65
|
+
api_path = API_DIR / f"{api_name_lower}.py"
|
|
66
|
+
api_content = f"""from z8ter.api import API
|
|
67
|
+
from starlette.requests import Request
|
|
68
|
+
from starlette.responses import JSONResponse
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class {class_name}(Page):
|
|
72
|
+
@API.endpoint("GET", "/")
|
|
73
|
+
async def get_{class_name}(self, request: Request) -> JSONResponse:
|
|
74
|
+
return JSONResponse({{"message": "Hello from {class_name} API!"}}, 200)
|
|
75
|
+
"""
|
|
76
|
+
if not api_path.exists():
|
|
77
|
+
api_path.write_text(api_content, encoding="utf-8")
|
|
78
|
+
print(f"Created template: {api_path}")
|
|
79
|
+
else:
|
|
80
|
+
print(f"Template already exists: {api_path}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def new_project(project_name: str):
|
|
84
|
+
print("This feature has not been implemented yet.")
|
|
85
|
+
print(f"You can start your project {project_name} by cloning the repo:")
|
|
86
|
+
print("https://github.com/ashesh808/Z8ter")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_server(
|
|
90
|
+
mode: str = "prod", app_path: str = "main:app",
|
|
91
|
+
host: str = "127.0.0.1", port: int = 8000
|
|
92
|
+
):
|
|
93
|
+
import uvicorn
|
|
94
|
+
uvicorn.run(app_path, host=host, port=port, reload=(mode == "dev"))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def main():
|
|
98
|
+
parser = argparse.ArgumentParser(prog="z8", description="Z8ter CLI")
|
|
99
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
100
|
+
|
|
101
|
+
p_create = sub.add_parser("create_page",
|
|
102
|
+
help="Create a new page (template + view)")
|
|
103
|
+
p_create.add_argument("name",
|
|
104
|
+
help="Page name (e.g., 'home' or 'app/home')")
|
|
105
|
+
p_new = sub.add_parser("new",
|
|
106
|
+
help="Create a new Z8ter project")
|
|
107
|
+
p_new.add_argument("project_name",
|
|
108
|
+
help="Folder name for the new project")
|
|
109
|
+
p_run = sub.add_parser("run", help="Run the app (default: prod)")
|
|
110
|
+
p_run.add_argument("mode", nargs="?", choices=["dev"],
|
|
111
|
+
help="Use 'dev' for autoreload")
|
|
112
|
+
args = parser.parse_args()
|
|
113
|
+
if args.cmd == "create_page":
|
|
114
|
+
create_page(args.name)
|
|
115
|
+
elif args.cmd == "new":
|
|
116
|
+
new_project(args.project_name)
|
|
117
|
+
elif args.cmd == "run":
|
|
118
|
+
run_server(mode="dev" if args.mode == "dev" else "prod")
|
|
119
|
+
else:
|
|
120
|
+
parser.print_help()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from starlette.config import Config
|
|
2
|
+
from z8ter import BASE_DIR, FAVICON_PATH, VIEWS_DIR
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def build_config(env_file: str) -> Config:
|
|
6
|
+
cf = Config(env_file)
|
|
7
|
+
cf.file_values["ROOT"] = str(BASE_DIR)
|
|
8
|
+
cf.file_values["FAVICON_PATH"] = str(FAVICON_PATH)
|
|
9
|
+
cf.file_values["VIEW_PATH"] = str(VIEWS_DIR)
|
|
10
|
+
return cf
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import logging
|
|
4
|
+
from starlette.applications import Starlette
|
|
5
|
+
from starlette.routing import Route
|
|
6
|
+
from starlette.middleware.sessions import SessionMiddleware
|
|
7
|
+
from typing import List
|
|
8
|
+
import uvicorn
|
|
9
|
+
from starlette.staticfiles import StaticFiles
|
|
10
|
+
from z8ter import templates
|
|
11
|
+
from z8ter.router import build_routes_from_pages, build_routes_from_apis
|
|
12
|
+
logger = logging.getLogger("z8ter")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Z8ter:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
debug: bool | None = None,
|
|
20
|
+
mode: str | None = None,
|
|
21
|
+
views_dir: str | Path = "views",
|
|
22
|
+
routes: list | None = None,
|
|
23
|
+
sessions: bool = False,
|
|
24
|
+
session_secret: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self._extra_routes: list = list(routes or [])
|
|
27
|
+
self.mode = (mode or "prod").lower()
|
|
28
|
+
self.debug = bool(self.mode == "dev") if debug is None else bool(debug)
|
|
29
|
+
self.views_dir = Path(views_dir).resolve()
|
|
30
|
+
self.app = Starlette(debug=self.debug, routes=self._assemble_routes())
|
|
31
|
+
if sessions:
|
|
32
|
+
if session_secret:
|
|
33
|
+
secret = session_secret
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"Z8ter: session_secret is required when sessions=True."
|
|
37
|
+
)
|
|
38
|
+
self.app.add_middleware(SessionMiddleware, secret_key=secret)
|
|
39
|
+
static_dir = Path("static")
|
|
40
|
+
if static_dir.exists():
|
|
41
|
+
self.app.mount("/static", StaticFiles(directory=str(static_dir)),
|
|
42
|
+
name="static")
|
|
43
|
+
|
|
44
|
+
def _url_for(name: str, filename: str | None = None, **params):
|
|
45
|
+
if filename is not None:
|
|
46
|
+
params["path"] = filename
|
|
47
|
+
return self.app.url_path_for(name, **params)
|
|
48
|
+
|
|
49
|
+
templates.env.globals["url_for"] = _url_for
|
|
50
|
+
|
|
51
|
+
def _assemble_routes(self) -> List[Route]:
|
|
52
|
+
routes = []
|
|
53
|
+
routes += self._extra_routes
|
|
54
|
+
if self.debug:
|
|
55
|
+
logger.warning("🚀 Z8ter running in DEV mode")
|
|
56
|
+
else:
|
|
57
|
+
logger.info("🚀 Z8ter running in PROD mode")
|
|
58
|
+
routes += build_routes_from_pages()
|
|
59
|
+
routes += build_routes_from_apis()
|
|
60
|
+
return routes
|
|
61
|
+
|
|
62
|
+
def _ensure_services_registry(self) -> None:
|
|
63
|
+
if not hasattr(self.app.state, "services"):
|
|
64
|
+
self.app.state.services = {}
|
|
65
|
+
|
|
66
|
+
def add_service(
|
|
67
|
+
self, obj: object, *,
|
|
68
|
+
replace: bool = False
|
|
69
|
+
) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Registers a process-wide service under app.state.
|
|
72
|
+
Access via: request.app.state.<name> or
|
|
73
|
+
request.app.state.services[name]
|
|
74
|
+
"""
|
|
75
|
+
self._ensure_services_registry()
|
|
76
|
+
key = (obj.__class__.__name__).rstrip("_").lower()
|
|
77
|
+
|
|
78
|
+
if key in self.app.state.services and not replace:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Service '{key}' already exists." +
|
|
81
|
+
"Use replace=True to overwrite."
|
|
82
|
+
)
|
|
83
|
+
self.app.state.services[key] = obj
|
|
84
|
+
setattr(self.app.state, key, obj)
|
|
85
|
+
return key
|
|
86
|
+
|
|
87
|
+
async def __call__(self, scope, receive, send):
|
|
88
|
+
await self.app(scope, receive, send)
|
|
89
|
+
|
|
90
|
+
def run(
|
|
91
|
+
self,
|
|
92
|
+
host: str = "127.0.0.1",
|
|
93
|
+
port: int = 8080,
|
|
94
|
+
reload: bool | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
reload = self.debug if reload is None else reload
|
|
97
|
+
uvicorn.run(
|
|
98
|
+
"main:app" if reload else self,
|
|
99
|
+
host=host,
|
|
100
|
+
port=port,
|
|
101
|
+
reload=reload
|
|
102
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from starlette.requests import Request
|
|
3
|
+
from starlette.responses import Response
|
|
4
|
+
from starlette.endpoints import HTTPEndpoint
|
|
5
|
+
from z8ter import templates
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Page(HTTPEndpoint):
|
|
9
|
+
"""HTTPEndpoint + a small render() helper for templates."""
|
|
10
|
+
def __init_subclass__(cls, **kwargs):
|
|
11
|
+
super().__init_subclass__(**kwargs)
|
|
12
|
+
mod = cls.__module__
|
|
13
|
+
if mod.startswith("views."):
|
|
14
|
+
pid = mod.removeprefix("views.")
|
|
15
|
+
else:
|
|
16
|
+
pid = mod
|
|
17
|
+
cls._page_id = pid
|
|
18
|
+
|
|
19
|
+
def __init__(self, scope=None, receive=None, send=None):
|
|
20
|
+
if scope is not None and receive is not None and send is not None:
|
|
21
|
+
super().__init__(scope, receive, send)
|
|
22
|
+
|
|
23
|
+
def render(self, request: Request, template_name: str,
|
|
24
|
+
context: dict | None = None) -> Response:
|
|
25
|
+
page_id = getattr(self.__class__, "_page_id", None)
|
|
26
|
+
ctx = {"page_id": page_id, "request": request}
|
|
27
|
+
if context:
|
|
28
|
+
ctx.update(context)
|
|
29
|
+
return templates.TemplateResponse(template_name, ctx)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import importlib
|
|
4
|
+
from typing import Iterable, Type
|
|
5
|
+
from starlette.routing import Route, Mount
|
|
6
|
+
from starlette.endpoints import HTTPEndpoint
|
|
7
|
+
from z8ter.api import API
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _import_module_for(rel_path: Path, package_root: str) -> object:
|
|
11
|
+
mod_name = str(rel_path.with_suffix("")).replace(
|
|
12
|
+
"/", ".").replace("\\", ".")
|
|
13
|
+
if mod_name.startswith(f"{package_root}."):
|
|
14
|
+
full_name = mod_name
|
|
15
|
+
else:
|
|
16
|
+
full_name = f"{package_root}.{mod_name}"
|
|
17
|
+
return importlib.import_module(full_name)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _iter_page_classes(mod) -> Iterable[Type[HTTPEndpoint]]:
|
|
21
|
+
from .page import Page
|
|
22
|
+
for obj in vars(mod).values():
|
|
23
|
+
if isinstance(obj, type) and issubclass(obj, Page) and obj is not Page:
|
|
24
|
+
yield obj
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _iter_api_classes(mod) -> Iterable[Type[API]]:
|
|
28
|
+
from .api import API
|
|
29
|
+
for obj in vars(mod).values():
|
|
30
|
+
if isinstance(obj, type) and issubclass(obj, API) and obj is not API:
|
|
31
|
+
yield obj
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _url_from_file(pages_root: Path, file_path: Path) -> str:
|
|
35
|
+
rel = file_path.relative_to(pages_root).with_suffix("")
|
|
36
|
+
parts = rel.parts
|
|
37
|
+
url = "/" + "/".join(parts)
|
|
38
|
+
if url.endswith("/index"):
|
|
39
|
+
url = url[:-len("/index")] or "/"
|
|
40
|
+
return url or "/"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_routes_from_pages(pages_dir: str = "views") -> list[Route]:
|
|
44
|
+
routes: list[Route] = []
|
|
45
|
+
pages_root = Path(pages_dir).resolve()
|
|
46
|
+
seen_paths: set[str] = set()
|
|
47
|
+
for file_path in pages_root.rglob("*.py"):
|
|
48
|
+
if file_path.name == "__init__.py":
|
|
49
|
+
continue
|
|
50
|
+
rel_to_cwd = file_path.relative_to(Path().resolve())
|
|
51
|
+
mod = _import_module_for(rel_to_cwd, pages_dir)
|
|
52
|
+
classes = list(_iter_page_classes(mod))
|
|
53
|
+
if not classes:
|
|
54
|
+
continue
|
|
55
|
+
base_path = _url_from_file(pages_root, file_path)
|
|
56
|
+
for cls in classes:
|
|
57
|
+
path = getattr(cls, "path", None) or base_path
|
|
58
|
+
if path in seen_paths and getattr(cls, "path", None) is None:
|
|
59
|
+
path = f"{base_path}/{cls.__name__.lower()}"
|
|
60
|
+
if path not in seen_paths:
|
|
61
|
+
routes.append(Route(path, cls))
|
|
62
|
+
seen_paths.add(path)
|
|
63
|
+
return routes
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_routes_from_apis(api_dir: str = "api") -> list[Mount]:
|
|
67
|
+
routes: list[Mount] = []
|
|
68
|
+
pages_root = Path(api_dir).resolve()
|
|
69
|
+
for file_path in pages_root.rglob("*.py"):
|
|
70
|
+
if file_path.name == "__init__.py":
|
|
71
|
+
continue
|
|
72
|
+
rel_to_cwd = file_path.relative_to(Path().resolve())
|
|
73
|
+
mod = _import_module_for(rel_to_cwd, api_dir)
|
|
74
|
+
classes = list(_iter_api_classes(mod))
|
|
75
|
+
if not classes:
|
|
76
|
+
continue
|
|
77
|
+
for cls in classes:
|
|
78
|
+
routes.append(cls.build_mount())
|
|
79
|
+
return routes
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_favicon_route(api_dir: str = "static/favicon") -> list[Mount]:
|
|
83
|
+
routes: list[Mount] = []
|
|
84
|
+
pages_root = Path(api_dir).resolve()
|
|
85
|
+
for file_path in pages_root.rglob("*.py"):
|
|
86
|
+
if file_path.name == "__init__.py":
|
|
87
|
+
continue
|
|
88
|
+
rel_to_cwd = file_path.relative_to(Path().resolve())
|
|
89
|
+
mod = _import_module_for(rel_to_cwd, api_dir)
|
|
90
|
+
classes = list(_iter_api_classes(mod))
|
|
91
|
+
if not classes:
|
|
92
|
+
continue
|
|
93
|
+
for cls in classes:
|
|
94
|
+
routes.append(cls.build_mount())
|
|
95
|
+
return routes
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: z8ter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SSR-first Python web framework with file-based views, tinyCSR islands, and decorator-driven APIs (Starlette + Jinja2).
|
|
5
|
+
Home-page: https://github.com/ashesh808/Z8ter
|
|
6
|
+
Author: Ashesh Nepal
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: starlette<1.0,>=0.47
|
|
21
|
+
Requires-Dist: Jinja2<4.0,>=3.1
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: license
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
Dynamic: requires-dist
|
|
30
|
+
Dynamic: requires-python
|
|
31
|
+
Dynamic: summary
|
|
32
|
+
|
|
33
|
+
# Z8ter.py
|
|
34
|
+
|
|
35
|
+
**Z8ter** is a lightweight, Laravel-inspired full-stack Python web framework built on [Starlette], designed for rapid development with tight integration between backend logic and frontend templates—plus small client-side “islands” where they make sense.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## ✨ Features (Current)
|
|
40
|
+
|
|
41
|
+
### 1) File-Based Views (SSR)
|
|
42
|
+
- Files under `views/` become routes automatically.
|
|
43
|
+
- Each view pairs Python logic with a Jinja template in `templates/`.
|
|
44
|
+
- A stable `page_id` (derived from `views/` path) is injected into templates and used by the frontend loader to hydrate per-page JS.
|
|
45
|
+
|
|
46
|
+
### 2) Jinja2 Templating
|
|
47
|
+
- Template inheritance with `{% extends %}` / `{% block %}`.
|
|
48
|
+
- Templates live in `templates/` (default extension: `.jinja`).
|
|
49
|
+
|
|
50
|
+
### 3) Small CSR “Islands”
|
|
51
|
+
- A tiny client router lazy-loads `/static/js/pages/<page_id>.js` and runs its default export.
|
|
52
|
+
- Great for interactive bits (theme toggles, pings, clipboard, etc.) without going full SPA.
|
|
53
|
+
|
|
54
|
+
### 4) Decorator-Driven APIs
|
|
55
|
+
- Classes under `api/` subclass `API` and register endpoints with a decorator.
|
|
56
|
+
- Each class mounts under `/api/<id>` (derived from module path).
|
|
57
|
+
|
|
58
|
+
> Example shape (conceptual):
|
|
59
|
+
> ```
|
|
60
|
+
> api/hello.py → /api/hello
|
|
61
|
+
> views/about.py → /about
|
|
62
|
+
> templates/about.jinja + static/js/pages/about.js (island)
|
|
63
|
+
> ```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🚀 Getting Started
|
|
68
|
+
|
|
69
|
+
### Prerequisites
|
|
70
|
+
- Python 3.11+ and `pip`
|
|
71
|
+
- Node 18+ and `npm`
|
|
72
|
+
|
|
73
|
+
### Install & Run (dev)
|
|
74
|
+
```bash
|
|
75
|
+
# 1) Python deps (in a venv)
|
|
76
|
+
python -m venv .venv
|
|
77
|
+
source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1
|
|
78
|
+
pip install -r requirements.txt # or: pip install -e .
|
|
79
|
+
|
|
80
|
+
# 2) Frontend deps
|
|
81
|
+
npm install
|
|
82
|
+
|
|
83
|
+
# 3) Dev server(s)
|
|
84
|
+
npm run dev
|
|
85
|
+
````
|
|
86
|
+
|
|
87
|
+
> `npm run dev` runs the dev workflow (backend + assets). Check the terminal for the local URL.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 📁 Project Structure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
.
|
|
95
|
+
├─ api/ # API classes (@API.endpoint)
|
|
96
|
+
│ └─ hello.py
|
|
97
|
+
├─ views/ # File-based pages (SSR)
|
|
98
|
+
│ └─ index.py
|
|
99
|
+
├─ templates/ # Jinja templates
|
|
100
|
+
│ ├─ base.jinja
|
|
101
|
+
│ └─ index.jinja
|
|
102
|
+
├─ static/
|
|
103
|
+
│ └─ js/
|
|
104
|
+
│ └─ pages/ # Per-page islands: about.js, app/home.js, ...
|
|
105
|
+
│ └─ common.js
|
|
106
|
+
├─ z8ter/ # Framework core (Page, API, router)
|
|
107
|
+
└─ main.py # App entrypoint
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 🧩 Usage Examples
|
|
113
|
+
|
|
114
|
+
### View + Template (SSR)
|
|
115
|
+
|
|
116
|
+
```jinja
|
|
117
|
+
{# templates/index.jinja #}
|
|
118
|
+
{% extends "base.jinja" %}
|
|
119
|
+
{% block content %}
|
|
120
|
+
<h1>{{ title }}</h1>
|
|
121
|
+
<div id="api-response"></div>
|
|
122
|
+
{% endblock %}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Client Island (runs when `page_id` matches)
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// static/js/pages/common.ts (or a specific page module)
|
|
129
|
+
export default async function init() {
|
|
130
|
+
// hydrate interactive bits, fetch data, etc.
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Minimal API Class
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# api/hello.py
|
|
138
|
+
from z8ter.api import API
|
|
139
|
+
|
|
140
|
+
class Hello(API):
|
|
141
|
+
@API.endpoint("GET", "/hello")
|
|
142
|
+
async def hello(self, request):
|
|
143
|
+
return {"ok": True, "message": "Hello from Z8ter"}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 🛣️ Planned
|
|
149
|
+
|
|
150
|
+
* **CLI scaffolding**: `z8 new`, `z8 dev`, `z8 create_page <name>`
|
|
151
|
+
* **Auth scaffolding**: login/register/logout + session helpers
|
|
152
|
+
* **Stripe integration**: pricing page, checkout routes, webhooks
|
|
153
|
+
* **DB adapters**: SQLite default, Postgres option
|
|
154
|
+
* **HTMX + Tailwind/DaisyUI** polish out of the box
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 🧠 Philosophy
|
|
159
|
+
|
|
160
|
+
* Conventions over configuration
|
|
161
|
+
* SSR-first with tiny CSR islands
|
|
162
|
+
* Small surface area; sharp, pragmatic tools
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
api/__init__.py
|
|
6
|
+
api/hello.py
|
|
7
|
+
views/__init__.py
|
|
8
|
+
views/about.py
|
|
9
|
+
views/index.py
|
|
10
|
+
z8ter/__init__.py
|
|
11
|
+
z8ter/api.py
|
|
12
|
+
z8ter/cli.py
|
|
13
|
+
z8ter/config.py
|
|
14
|
+
z8ter/core.py
|
|
15
|
+
z8ter/page.py
|
|
16
|
+
z8ter/router.py
|
|
17
|
+
z8ter.egg-info/PKG-INFO
|
|
18
|
+
z8ter.egg-info/SOURCES.txt
|
|
19
|
+
z8ter.egg-info/dependency_links.txt
|
|
20
|
+
z8ter.egg-info/entry_points.txt
|
|
21
|
+
z8ter.egg-info/requires.txt
|
|
22
|
+
z8ter.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|