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 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
@@ -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)
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
z8ter-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,8 @@
1
+ from z8ter.page import Page
2
+ from starlette.requests import Request
3
+ from starlette.responses import Response
4
+
5
+
6
+ class About(Page):
7
+ async def get(self, request: Request) -> Response:
8
+ return self.render(request, "about.jinja", {})
@@ -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))
@@ -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
@@ -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,2 @@
1
+ [console_scripts]
2
+ z8 = z8ter.cli:main
@@ -0,0 +1,2 @@
1
+ starlette<1.0,>=0.47
2
+ Jinja2<4.0,>=3.1
@@ -0,0 +1,3 @@
1
+ api
2
+ views
3
+ z8ter