astris-python 0.1.0__py3-none-any.whl
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.
- astris/__init__.py +7 -0
- astris/assets/favicon.ico +0 -0
- astris/auth/__init__.py +29 -0
- astris/auth/installer.py +512 -0
- astris/auth/session.py +241 -0
- astris/cli.py +421 -0
- astris/config.py +50 -0
- astris/database/__init__.py +28 -0
- astris/database/migrations.py +259 -0
- astris/database/session.py +108 -0
- astris/http/__init__.py +25 -0
- astris/http/static.py +31 -0
- astris/inertia/__init__.py +8 -0
- astris/inertia/exceptions.py +133 -0
- astris/inertia/response.py +99 -0
- astris/inertia/shared.py +176 -0
- astris/inertia/vite.py +89 -0
- astris/installer.py +595 -0
- astris/kernel.py +230 -0
- astris/py.typed +0 -0
- astris/routing/__init__.py +31 -0
- astris/routing/router.py +38 -0
- astris/security/__init__.py +3 -0
- astris/security/csrf.py +100 -0
- astris_python-0.1.0.dist-info/METADATA +241 -0
- astris_python-0.1.0.dist-info/RECORD +29 -0
- astris_python-0.1.0.dist-info/WHEEL +4 -0
- astris_python-0.1.0.dist-info/entry_points.txt +5 -0
- astris_python-0.1.0.dist-info/licenses/LICENSE +21 -0
astris/kernel.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import pkgutil
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable, Coroutine, Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, FastAPI, HTTPException
|
|
10
|
+
from fastapi.exceptions import RequestValidationError
|
|
11
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
14
|
+
from starlette.middleware.sessions import SessionMiddleware
|
|
15
|
+
from starlette.staticfiles import StaticFiles
|
|
16
|
+
|
|
17
|
+
from astris.config import Settings, settings
|
|
18
|
+
from astris.database import db
|
|
19
|
+
from astris.http.static import PublicStaticMiddleware
|
|
20
|
+
from astris.inertia import share
|
|
21
|
+
from astris.inertia.exceptions import (
|
|
22
|
+
inertia_http_exception_handler,
|
|
23
|
+
inertia_validation_exception_handler,
|
|
24
|
+
)
|
|
25
|
+
from astris.inertia.shared import FlashMiddleware
|
|
26
|
+
from astris.security import CSRFMiddleware
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Astris:
|
|
30
|
+
"""Core framework application class."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
base_path: Path | None = None,
|
|
35
|
+
config: Settings | None = None,
|
|
36
|
+
title: str | None = None,
|
|
37
|
+
cors_origins: Sequence[str] | None = None,
|
|
38
|
+
enable_csrf: bool | None = None,
|
|
39
|
+
csrf_exempt_paths: Sequence[str] | None = None,
|
|
40
|
+
shared_props: dict[str, Any] | None = None,
|
|
41
|
+
database_url: str | None = None,
|
|
42
|
+
auto_create_tables: bool | None = None,
|
|
43
|
+
db_echo: bool | None = None,
|
|
44
|
+
secret_key: str | None = None,
|
|
45
|
+
session_cookie_name: str | None = None,
|
|
46
|
+
session_max_age: int | None = None,
|
|
47
|
+
session_https_only: bool | None = None,
|
|
48
|
+
session_same_site: Literal["lax", "strict", "none"] | None = None,
|
|
49
|
+
**fastapi_kwargs: Any,
|
|
50
|
+
):
|
|
51
|
+
self.base_path = base_path or Path.cwd()
|
|
52
|
+
self.config = config or settings
|
|
53
|
+
|
|
54
|
+
self.cors_origins: Sequence[str] = (
|
|
55
|
+
cors_origins if cors_origins is not None else self.config.cors_origins
|
|
56
|
+
)
|
|
57
|
+
self.enable_csrf = (
|
|
58
|
+
enable_csrf if enable_csrf is not None else self.config.enable_csrf
|
|
59
|
+
)
|
|
60
|
+
self.csrf_exempt_paths: Sequence[str] = (
|
|
61
|
+
csrf_exempt_paths
|
|
62
|
+
if csrf_exempt_paths is not None
|
|
63
|
+
else self.config.csrf_exempt_paths
|
|
64
|
+
)
|
|
65
|
+
self.auto_create_tables = (
|
|
66
|
+
auto_create_tables
|
|
67
|
+
if auto_create_tables is not None
|
|
68
|
+
else self.config.auto_create_tables
|
|
69
|
+
)
|
|
70
|
+
self.secret_key = secret_key or self.config.app_key
|
|
71
|
+
self.session_cookie_name = (
|
|
72
|
+
session_cookie_name or self.config.session_cookie_name
|
|
73
|
+
)
|
|
74
|
+
self.session_max_age = (
|
|
75
|
+
session_max_age
|
|
76
|
+
if session_max_age is not None
|
|
77
|
+
else self.config.session_max_age
|
|
78
|
+
)
|
|
79
|
+
self.session_https_only = (
|
|
80
|
+
session_https_only
|
|
81
|
+
if session_https_only is not None
|
|
82
|
+
else self.config.session_https_only
|
|
83
|
+
)
|
|
84
|
+
self.session_same_site = (
|
|
85
|
+
session_same_site or self.config.session_same_site # type: ignore[assignment]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Configure database engine
|
|
89
|
+
db.configure(
|
|
90
|
+
url=database_url or self.config.database_url,
|
|
91
|
+
echo=db_echo if db_echo is not None else self.config.db_echo,
|
|
92
|
+
base_path=self.base_path,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if shared_props:
|
|
96
|
+
share(shared_props)
|
|
97
|
+
|
|
98
|
+
# Ensure project root is in sys.path for dynamic imports
|
|
99
|
+
base_path_str = str(self.base_path)
|
|
100
|
+
if base_path_str not in sys.path:
|
|
101
|
+
sys.path.insert(0, base_path_str)
|
|
102
|
+
|
|
103
|
+
app_title = title or self.config.app_name
|
|
104
|
+
app_debug = self.config.app_debug
|
|
105
|
+
self.app = FastAPI(title=app_title, debug=app_debug, **fastapi_kwargs)
|
|
106
|
+
self._boot()
|
|
107
|
+
|
|
108
|
+
def _boot(self) -> None:
|
|
109
|
+
self._configure_middleware()
|
|
110
|
+
self._configure_exception_handlers()
|
|
111
|
+
self._mount_static()
|
|
112
|
+
self._discover_modules()
|
|
113
|
+
if self.auto_create_tables:
|
|
114
|
+
db.create_all()
|
|
115
|
+
|
|
116
|
+
def _configure_exception_handlers(self) -> None:
|
|
117
|
+
"""Register Inertia validation and HTTP exception handlers."""
|
|
118
|
+
self.app.add_exception_handler(
|
|
119
|
+
RequestValidationError,
|
|
120
|
+
inertia_validation_exception_handler,
|
|
121
|
+
)
|
|
122
|
+
self.app.add_exception_handler(
|
|
123
|
+
ValidationError,
|
|
124
|
+
inertia_validation_exception_handler,
|
|
125
|
+
)
|
|
126
|
+
self.app.add_exception_handler(
|
|
127
|
+
HTTPException,
|
|
128
|
+
inertia_http_exception_handler,
|
|
129
|
+
)
|
|
130
|
+
self.app.add_exception_handler(
|
|
131
|
+
StarletteHTTPException,
|
|
132
|
+
inertia_http_exception_handler,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _configure_middleware(self) -> None:
|
|
136
|
+
"""Register middleware stack (configured innermost to outermost)."""
|
|
137
|
+
# 1. CSRF protection (innermost HTTP security)
|
|
138
|
+
if self.enable_csrf:
|
|
139
|
+
self.app.add_middleware(
|
|
140
|
+
CSRFMiddleware,
|
|
141
|
+
exempt_paths=self.csrf_exempt_paths,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# 2. Flash message persistence
|
|
145
|
+
self.app.add_middleware(FlashMiddleware)
|
|
146
|
+
|
|
147
|
+
# 3. Encrypted/signed cookie sessions
|
|
148
|
+
if self.secret_key:
|
|
149
|
+
self.app.add_middleware(
|
|
150
|
+
SessionMiddleware,
|
|
151
|
+
secret_key=self.secret_key,
|
|
152
|
+
session_cookie=self.session_cookie_name,
|
|
153
|
+
max_age=self.session_max_age,
|
|
154
|
+
https_only=self.session_https_only,
|
|
155
|
+
same_site=self.session_same_site,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# 4. Public static assets (serves /favicon.ico, /robots.txt, and public/ files directly)
|
|
159
|
+
public_dir = self.base_path / "public"
|
|
160
|
+
if public_dir.exists():
|
|
161
|
+
self.app.add_middleware(PublicStaticMiddleware, public_dir=public_dir)
|
|
162
|
+
|
|
163
|
+
# 5. CORS configuration (outermost to handle preflight)
|
|
164
|
+
self.app.add_middleware(
|
|
165
|
+
CORSMiddleware,
|
|
166
|
+
allow_origins=list(self.cors_origins),
|
|
167
|
+
allow_credentials=True,
|
|
168
|
+
allow_methods=["*"],
|
|
169
|
+
allow_headers=["*"],
|
|
170
|
+
expose_headers=["X-Inertia", "X-XSRF-TOKEN"],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _mount_static(self) -> None:
|
|
174
|
+
"""Mount compiled Vite frontend assets."""
|
|
175
|
+
build_dir = self.base_path / "public" / "build"
|
|
176
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
self.app.mount(
|
|
178
|
+
"/build",
|
|
179
|
+
StaticFiles(directory=str(build_dir), check_dir=False),
|
|
180
|
+
name="build",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _discover_modules(self) -> None:
|
|
184
|
+
modules_dir = self.base_path / "app" / "modules"
|
|
185
|
+
if not modules_dir.exists():
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
registered_routers: set[int] = set()
|
|
189
|
+
|
|
190
|
+
for _, modname, ispkg in pkgutil.walk_packages(
|
|
191
|
+
[str(modules_dir)], prefix="app.modules."
|
|
192
|
+
):
|
|
193
|
+
if ispkg:
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
last_part = modname.split(".")[-1]
|
|
197
|
+
|
|
198
|
+
# Auto-import models to register SQLModel table metadata
|
|
199
|
+
if last_part.endswith(("_model", "_models")) or last_part in (
|
|
200
|
+
"models",
|
|
201
|
+
"model",
|
|
202
|
+
):
|
|
203
|
+
try:
|
|
204
|
+
importlib.import_module(modname)
|
|
205
|
+
except ImportError:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# Only scan controller files
|
|
209
|
+
if not last_part.endswith("_controller") and not last_part.startswith(
|
|
210
|
+
"controller"
|
|
211
|
+
):
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
module = importlib.import_module(modname)
|
|
215
|
+
for _, member in inspect.getmembers(module):
|
|
216
|
+
if (
|
|
217
|
+
isinstance(member, APIRouter)
|
|
218
|
+
and id(member) not in registered_routers
|
|
219
|
+
):
|
|
220
|
+
self.app.include_router(member)
|
|
221
|
+
registered_routers.add(id(member))
|
|
222
|
+
|
|
223
|
+
async def __call__(
|
|
224
|
+
self,
|
|
225
|
+
scope: Any,
|
|
226
|
+
receive: Callable[..., Coroutine[Any, Any, Any]],
|
|
227
|
+
send: Callable[..., Coroutine[Any, Any, None]],
|
|
228
|
+
) -> None:
|
|
229
|
+
"""ASGI 3 interface."""
|
|
230
|
+
await self.app(scope, receive, send)
|
astris/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from astris.routing.router import (
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Cookie,
|
|
5
|
+
Depends,
|
|
6
|
+
File,
|
|
7
|
+
Form,
|
|
8
|
+
Header,
|
|
9
|
+
Path,
|
|
10
|
+
PathParam,
|
|
11
|
+
Query,
|
|
12
|
+
Security,
|
|
13
|
+
UploadFile,
|
|
14
|
+
status,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Body",
|
|
19
|
+
"Controller",
|
|
20
|
+
"Cookie",
|
|
21
|
+
"Depends",
|
|
22
|
+
"File",
|
|
23
|
+
"Form",
|
|
24
|
+
"Header",
|
|
25
|
+
"Path",
|
|
26
|
+
"PathParam",
|
|
27
|
+
"Query",
|
|
28
|
+
"Security",
|
|
29
|
+
"UploadFile",
|
|
30
|
+
"status",
|
|
31
|
+
]
|
astris/routing/router.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from fastapi import (
|
|
2
|
+
APIRouter,
|
|
3
|
+
Body,
|
|
4
|
+
Cookie,
|
|
5
|
+
Depends,
|
|
6
|
+
File,
|
|
7
|
+
Form,
|
|
8
|
+
Header,
|
|
9
|
+
Path,
|
|
10
|
+
Query,
|
|
11
|
+
Security,
|
|
12
|
+
UploadFile,
|
|
13
|
+
status,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Alias to avoid name collisions with standard library pathlib.Path
|
|
17
|
+
PathParam = Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Controller(APIRouter):
|
|
21
|
+
"""Core Astris Controller router."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Body",
|
|
26
|
+
"Controller",
|
|
27
|
+
"Cookie",
|
|
28
|
+
"Depends",
|
|
29
|
+
"File",
|
|
30
|
+
"Form",
|
|
31
|
+
"Header",
|
|
32
|
+
"Path",
|
|
33
|
+
"PathParam",
|
|
34
|
+
"Query",
|
|
35
|
+
"Security",
|
|
36
|
+
"UploadFile",
|
|
37
|
+
"status",
|
|
38
|
+
]
|
astris/security/csrf.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from starlette.datastructures import MutableHeaders
|
|
6
|
+
from starlette.requests import Request
|
|
7
|
+
from starlette.responses import JSONResponse
|
|
8
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CSRFMiddleware:
|
|
12
|
+
"""Cookie-to-header CSRF protection middleware.
|
|
13
|
+
|
|
14
|
+
Sets an XSRF-TOKEN cookie on responses and validates the incoming
|
|
15
|
+
X-XSRF-TOKEN (or X-CSRF-TOKEN) header against the cookie on state-changing requests.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
SAFE_METHODS: ClassVar[frozenset[str]] = frozenset({"GET", "HEAD", "OPTIONS"})
|
|
19
|
+
COOKIE_NAME: ClassVar[str] = "XSRF-TOKEN"
|
|
20
|
+
HEADER_NAMES: ClassVar[tuple[str, ...]] = ("x-xsrf-token", "x-csrf-token")
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
app: ASGIApp,
|
|
25
|
+
cookie_name: str = "XSRF-TOKEN",
|
|
26
|
+
cookie_path: str = "/",
|
|
27
|
+
cookie_domain: str | None = None,
|
|
28
|
+
cookie_secure: bool = False,
|
|
29
|
+
cookie_samesite: str = "lax",
|
|
30
|
+
exempt_paths: Sequence[str] = (),
|
|
31
|
+
) -> None:
|
|
32
|
+
self.app = app
|
|
33
|
+
self.cookie_name = cookie_name
|
|
34
|
+
self.cookie_path = cookie_path
|
|
35
|
+
self.cookie_domain = cookie_domain
|
|
36
|
+
self.cookie_secure = cookie_secure
|
|
37
|
+
self.cookie_samesite = cookie_samesite
|
|
38
|
+
self.exempt_paths = exempt_paths
|
|
39
|
+
|
|
40
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
41
|
+
if scope["type"] != "http":
|
|
42
|
+
await self.app(scope, receive, send)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
request = Request(scope, receive=receive)
|
|
46
|
+
cookie_token = request.cookies.get(self.cookie_name)
|
|
47
|
+
new_token: str | None = None
|
|
48
|
+
|
|
49
|
+
if not cookie_token:
|
|
50
|
+
cookie_token = secrets.token_urlsafe(32)
|
|
51
|
+
new_token = cookie_token
|
|
52
|
+
|
|
53
|
+
# Validate state-changing methods (POST, PUT, PATCH, DELETE)
|
|
54
|
+
if request.method.upper() not in self.SAFE_METHODS and not self._is_exempt(
|
|
55
|
+
request.url.path
|
|
56
|
+
):
|
|
57
|
+
header_token = None
|
|
58
|
+
for header_name in self.HEADER_NAMES:
|
|
59
|
+
if header_name in request.headers:
|
|
60
|
+
header_token = request.headers[header_name]
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
not header_token
|
|
65
|
+
or not cookie_token
|
|
66
|
+
or not secrets.compare_digest(header_token, cookie_token)
|
|
67
|
+
):
|
|
68
|
+
response = JSONResponse(
|
|
69
|
+
status_code=419,
|
|
70
|
+
content={"message": "CSRF token mismatch or missing."},
|
|
71
|
+
)
|
|
72
|
+
await response(scope, receive, send)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if new_token is not None:
|
|
76
|
+
token_str: str = new_token
|
|
77
|
+
|
|
78
|
+
async def send_wrapper(message: Message) -> None:
|
|
79
|
+
if message["type"] == "http.response.start":
|
|
80
|
+
headers = MutableHeaders(scope=message)
|
|
81
|
+
cookie_parts = [
|
|
82
|
+
f"{self.cookie_name}={token_str}",
|
|
83
|
+
f"Path={self.cookie_path}",
|
|
84
|
+
f"SameSite={self.cookie_samesite.capitalize()}",
|
|
85
|
+
]
|
|
86
|
+
if self.cookie_domain:
|
|
87
|
+
cookie_parts.append(f"Domain={self.cookie_domain}")
|
|
88
|
+
if self.cookie_secure:
|
|
89
|
+
cookie_parts.append("Secure")
|
|
90
|
+
|
|
91
|
+
headers.append("set-cookie", "; ".join(cookie_parts))
|
|
92
|
+
|
|
93
|
+
await send(message)
|
|
94
|
+
|
|
95
|
+
await self.app(scope, receive, send_wrapper)
|
|
96
|
+
else:
|
|
97
|
+
await self.app(scope, receive, send)
|
|
98
|
+
|
|
99
|
+
def _is_exempt(self, path: str) -> bool:
|
|
100
|
+
return any(path.startswith(exempt) for exempt in self.exempt_paths)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: astris-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The modern full-stack web framework for Python. Everything you need to go from idea to orbit; for developers who want to build and ship at escape velocity.
|
|
5
|
+
Author: Felix Gomez
|
|
6
|
+
Author-email: Felix Gomez <felix@felixgomez.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Framework :: Pydantic
|
|
14
|
+
Classifier: Framework :: Pydantic :: 2
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: Intended Audience :: System Administrators
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Internet
|
|
27
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
28
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
29
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
30
|
+
Classifier: Topic :: Software Development
|
|
31
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
32
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
33
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
34
|
+
Classifier: Typing :: Typed
|
|
35
|
+
Requires-Dist: alembic>=1.19.1
|
|
36
|
+
Requires-Dist: fastapi>=0.141.1
|
|
37
|
+
Requires-Dist: itsdangerous>=2.2.0
|
|
38
|
+
Requires-Dist: pwdlib[argon2]>=0.3.1
|
|
39
|
+
Requires-Dist: pydantic>=2.13.4
|
|
40
|
+
Requires-Dist: pydantic-settings>=2.15.0
|
|
41
|
+
Requires-Dist: sqlmodel>=0.0.39
|
|
42
|
+
Requires-Dist: typer>=0.27.1
|
|
43
|
+
Requires-Dist: uvicorn[standard]>=0.52.4
|
|
44
|
+
Requires-Python: >=3.11
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
<p align="center" style="padding: 20px 0 10px 0;">
|
|
48
|
+
<a href="https://github.com/TheFelixGomez/astris">
|
|
49
|
+
<img src="https://raw.githubusercontent.com/TheFelixGomez/astris/main/.github/assets/astris-logo-name.png" alt="Astris" width="380">
|
|
50
|
+
</a>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<strong>The modern full-stack web framework for Python.</strong><br>
|
|
55
|
+
Full-stack simplicity with modern Python performance.
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<p align="center">
|
|
59
|
+
<a href="https://pypi.org/project/astris-python/"><img src="https://img.shields.io/pypi/v/astris-python.svg" alt="PyPI version"></a>
|
|
60
|
+
<a href="https://pypi.org/project/astris-python/"><img src="https://img.shields.io/pypi/pyversions/astris-python.svg" alt="Python Versions"></a>
|
|
61
|
+
<a href="https://github.com/TheFelixGomez/astris/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
> ⚠️ **Early Alpha**: Astris is currently in active alpha development. APIs and features may undergo breaking changes until production-ready status is confirmed. Feedback, bug reports, and ideas are welcome!
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## What is Astris?
|
|
69
|
+
|
|
70
|
+
**Astris** bridges the gap between the rapid, joyful developer experience of classic full-stack frameworks (like Laravel and Rails) and the raw performance, modern typing, and async concurrency of **FastAPI** and **Python 3.11+**.
|
|
71
|
+
|
|
72
|
+
By combining **FastAPI**, **Inertia.js**, **Vue 3**, **Tailwind CSS v4**, and **SQLModel**, Astris lets you build rich, dynamic single-page applications without the overhead of maintaining separate API layers or complex state synchronization.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 🌟 Key Features
|
|
77
|
+
|
|
78
|
+
* **⚡ Lightning-Fast Core**: ASGI performance powered by FastAPI and Uvicorn with auto-generated OpenAPI & Swagger documentation.
|
|
79
|
+
* **🧩 Modern Monolith with Inertia.js**: Render Vue 3 components directly from FastAPI controllers with full server-side state hydration and zero REST boilerplate.
|
|
80
|
+
* **🛡️ Production-Ready Authentication**: Cryptographically signed cookie sessions, OWASP-standard **Argon2id** password hashing (`pwdlib`), and full-stack auth starter kit.
|
|
81
|
+
* **🗄️ SQLModel & Alembic Database Engine**: Unified declarative models, DTOs, and automatic schema migrations out-of-the-box.
|
|
82
|
+
* **⚙️ Type-Safe Centralized Configuration**: Powered by `pydantic-settings` for `.env` management, type casting, and fail-fast startup validation.
|
|
83
|
+
* **🪐 Orbit CLI**: An artisan developer CLI for scaffolding modules, generating migrations, and serving full-stack applications with hot-reloading.
|
|
84
|
+
* **🎨 Tailwind CSS v4 Pre-configured**: Instant zero-config styling powered by `@tailwindcss/vite`.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 🚀 Quickstart
|
|
89
|
+
|
|
90
|
+
Create a new full-stack Astris application in seconds using `uvx` (or `pipx`):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# 1. Create a new project
|
|
94
|
+
uvx --from astris-python astris new my_app
|
|
95
|
+
|
|
96
|
+
# Or install globally as a CLI tool:
|
|
97
|
+
# uv tool install astris-python
|
|
98
|
+
# astris new my_app
|
|
99
|
+
|
|
100
|
+
# 2. Enter directory and install frontend dependencies
|
|
101
|
+
cd my_app
|
|
102
|
+
npm install
|
|
103
|
+
|
|
104
|
+
# 3. Start full-stack development server (FastAPI + Vite HMR)
|
|
105
|
+
uv run orbit serve
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Open **`http://localhost:8000`** in your browser. Your full-stack app with authentication, Inertia.js, and SQLite is live!
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 💡 How It Feels
|
|
113
|
+
|
|
114
|
+
### 1. Controllers & Inertia Rendering
|
|
115
|
+
Write clean, expressive controllers that return frontend views or JSON seamlessly:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from astris.auth import AuthUser, auth_required
|
|
119
|
+
from astris.inertia import InertiaResponse
|
|
120
|
+
from astris.routing import Controller
|
|
121
|
+
|
|
122
|
+
controller = Controller(prefix="/dashboard", dependencies=[auth_required])
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@controller.get("/")
|
|
126
|
+
async def dashboard_page(request: Request, user: AuthUser) -> InertiaResponse:
|
|
127
|
+
# Props are passed directly to Vue 3 with $page.props
|
|
128
|
+
return InertiaResponse(request, "Dashboard", props={"username": user["name"]})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 2. Unified SQLModel Models
|
|
132
|
+
Define database tables and validation schemas in a single declarative model:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from astris.database import Field, SQLModel
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ArticleBase(SQLModel):
|
|
139
|
+
title: str = Field(index=True)
|
|
140
|
+
content: str
|
|
141
|
+
is_published: bool = Field(default=False)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Article(ArticleBase, table=True):
|
|
145
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class ArticleCreate(ArticleBase):
|
|
149
|
+
pass
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### 3. Frontend Views (Vue 3 + Inertia)
|
|
153
|
+
Build dynamic reactive pages without configuring client-side routers:
|
|
154
|
+
|
|
155
|
+
```vue
|
|
156
|
+
<script setup lang="ts">
|
|
157
|
+
import { Head, Link } from '@inertiajs/vue3'
|
|
158
|
+
|
|
159
|
+
defineProps<{
|
|
160
|
+
username: string
|
|
161
|
+
}>()
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<template>
|
|
165
|
+
<Head title="Dashboard" />
|
|
166
|
+
<div class="min-h-screen bg-slate-950 text-slate-100 p-8">
|
|
167
|
+
<h1 class="text-3xl font-bold">Welcome back, {{ username }}!</h1>
|
|
168
|
+
</div>
|
|
169
|
+
</template>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 🪐 The Orbit CLI
|
|
175
|
+
|
|
176
|
+
Astris comes equipped with **Orbit**, a developer toolkit for rapid development:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Start FastAPI backend + Vite frontend concurrently
|
|
180
|
+
uv run orbit serve
|
|
181
|
+
|
|
182
|
+
# Scaffold a new domain module (controller, service, model)
|
|
183
|
+
uv run orbit make:module billing
|
|
184
|
+
|
|
185
|
+
# Create and apply database migrations
|
|
186
|
+
uv run orbit make:migration "create_billing_table"
|
|
187
|
+
uv run orbit migrate
|
|
188
|
+
|
|
189
|
+
# Generate encryption keys
|
|
190
|
+
uv run orbit key:generate
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 📂 Project Architecture
|
|
196
|
+
|
|
197
|
+
Astris structures projects with a modular, **Domain-Driven Design** that grows gracefully:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
my_app/
|
|
201
|
+
├── app/
|
|
202
|
+
│ ├── core/
|
|
203
|
+
│ │ └── config.py # Centralized Pydantic settings & .env loading
|
|
204
|
+
│ └── modules/
|
|
205
|
+
│ ├── auth/ # Authentication domain (controller, service, model)
|
|
206
|
+
│ └── welcome/ # Welcome domain
|
|
207
|
+
├── database/
|
|
208
|
+
│ └── migrations/ # Alembic database migration versions
|
|
209
|
+
├── resources/
|
|
210
|
+
│ ├── css/
|
|
211
|
+
│ │ └── app.css # Tailwind CSS v4 styling
|
|
212
|
+
│ ├── js/
|
|
213
|
+
│ │ ├── Pages/ # Inertia Vue 3 views & components
|
|
214
|
+
│ │ └── app.ts # Vue entrypoint
|
|
215
|
+
│ └── views/
|
|
216
|
+
│ └── root.html # HTML shell template
|
|
217
|
+
├── .env # App configuration & APP_KEY
|
|
218
|
+
└── pyproject.toml # Python dependencies
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 🙏 Acknowledgments
|
|
224
|
+
|
|
225
|
+
Astris is built upon the work of giants. Huge gratitude to the incredible open-source projects and creators that make Astris possible:
|
|
226
|
+
|
|
227
|
+
* **[FastAPI](https://fastapi.tiangolo.com/) & [SQLModel](https://sqlmodel.tiangolo.com/)** by [Tiangolo](https://github.com/tiangolo) - for setting the standard in modern Python type safety, speed, and ergonomics.
|
|
228
|
+
* **[Inertia.js](https://inertiajs.com/)** by [Jonathan Reinink](https://github.com/reinink) - for the modern monolith architecture that bridges backend controllers and frontend SPAs without API boilerplate.
|
|
229
|
+
* **[Vue.js](https://vuejs.org/)** by [Evan You](https://github.com/yyx990803) - for the approachable and performant frontend component framework.
|
|
230
|
+
* **[Starlette](https://www.starlette.io/) & [Uvicorn](https://www.uvicorn.org/)** by [Encode](https://www.encode.io/) - for the lightning-fast ASGI toolkit and web server engine.
|
|
231
|
+
* **[Pydantic](https://docs.pydantic.dev/)** by [Samuel Colvin](https://github.com/samuelcolvin) & team - for rock-solid runtime data validation and centralized settings.
|
|
232
|
+
* **[Tailwind CSS](https://tailwindcss.com/)** by [Tailwind Labs](https://github.com/tailwindlabs) - for zero-config utility-first styling.
|
|
233
|
+
* **[Laravel](https://laravel.com/)** by [Taylor Otwell](https://github.com/taylorotwell) - for inspiring developer-first framework craftsmanship.
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 📄 License
|
|
238
|
+
|
|
239
|
+
The Astris framework is open source and licensed under the terms of the MIT license.
|
|
240
|
+
|
|
241
|
+
Built with ❤️ by [Felix Gomez](https://github.com/TheFelixGomez).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
astris/__init__.py,sha256=D7FkbtDHx3Kui4UTTYjuG24jguofpRADh6tTzbGgn3c,113
|
|
2
|
+
astris/assets/favicon.ico,sha256=qcGguPAGFmUGh4IhE859vx8EM7Euo6OQan5XxZcrLhw,15086
|
|
3
|
+
astris/auth/__init__.py,sha256=8T1YUiPbyhXy25SH-4Ko6CzHis6ReD2-_LpVuRNlcbc,539
|
|
4
|
+
astris/auth/installer.py,sha256=ZyBQui6aA5yNBN8X9HUp6UTVuwvO5W3tDVsag6TJtLQ,21631
|
|
5
|
+
astris/auth/session.py,sha256=elFjFQxhM-NqvcNQv7OPue0WRY7RUiitFlP_kC42vu4,7955
|
|
6
|
+
astris/cli.py,sha256=3a4_dCYpx2zTe8t3wgCyi42BRFn4indFcGaNucQN2Os,13518
|
|
7
|
+
astris/config.py,sha256=P15SJD-1NsnjFYYtEVeeBEq67w_sOTsSabkxAk8wKGc,1260
|
|
8
|
+
astris/database/__init__.py,sha256=8FFa-LEhkcNYxshIBOnUOMB6rKcLlLBNHVwlH7_X3Dk,375
|
|
9
|
+
astris/database/migrations.py,sha256=xjbiMpRsAI8LCtzpTaWhQmmPjuVhO-szTiX7Xny2Rms,7045
|
|
10
|
+
astris/database/session.py,sha256=9JNeG-UA3df7TQ7AVtaA9PjBjf__mlZF_oAQxpDWf1E,3151
|
|
11
|
+
astris/http/__init__.py,sha256=oa2xsHdse6FGNxO0bAqP1p9iySl6i_NtVopxOnYUzlw,504
|
|
12
|
+
astris/http/static.py,sha256=x6ysvjXVJd2pm6VbNt96VC7fT6rNbolF8Y43VG0cMVI,1191
|
|
13
|
+
astris/inertia/__init__.py,sha256=220WVzFB8ppZahAmrKyMmcmsyotfiyGeOnrClInL5jA,163
|
|
14
|
+
astris/inertia/exceptions.py,sha256=W-4Xi3WeSBa8FP5UKhT4XYmu-eoze6GgTkaDPo018W4,4583
|
|
15
|
+
astris/inertia/response.py,sha256=P3jjUGfL5Bpz2e4Wy4f7kylJ7P3_QeMTY4OfSfX3qnw,3213
|
|
16
|
+
astris/inertia/shared.py,sha256=loVqwOPE0N4WztNNn4wNWhPlj240xdLgx65RRUepyeU,6095
|
|
17
|
+
astris/inertia/vite.py,sha256=cqtwqPmdxsvsAV0Ja0YrY1TPHZ14oKipS9c7UaNALIY,3211
|
|
18
|
+
astris/installer.py,sha256=S8tNo6SF1MVlEWwDulEsG13cP6FBRMFc4qZKkKVFAK8,22042
|
|
19
|
+
astris/kernel.py,sha256=qaIcnf080BiRNoauHIOB6TPso0mjc5Aqp7aYD9DqV2I,8278
|
|
20
|
+
astris/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
astris/routing/__init__.py,sha256=g1urCz-1r8CkPae-7UMbIYh7YS_VeIafa7cGtol9vV0,401
|
|
22
|
+
astris/routing/router.py,sha256=NkFxYcqZCJeBVu0ncGLdYjSKg1JpKDfhr-73XEn3Vgc,530
|
|
23
|
+
astris/security/__init__.py,sha256=RxdeOT3ooBox0IXFZCiM2vOnm6q0k-T9QZPt7PbPAbs,78
|
|
24
|
+
astris/security/csrf.py,sha256=sO_rJ3qYAChLwNO545-OpTy-McwyPu2Rhqa_0Rhy1RI,3683
|
|
25
|
+
astris_python-0.1.0.dist-info/licenses/LICENSE,sha256=em4jHkBMoTK2MVxCtwUUBKAKJYEn2tuXviBDN_Fbdrw,1077
|
|
26
|
+
astris_python-0.1.0.dist-info/WHEEL,sha256=4OL6Foqnnp3xRY5wMkjgc25_i5YJC6dKsC6LPcjqEoU,80
|
|
27
|
+
astris_python-0.1.0.dist-info/entry_points.txt,sha256=dPbhCstaZa3L5pAfgbqrqTH4T7zwf6oRvjxt3GZqtqE,135
|
|
28
|
+
astris_python-0.1.0.dist-info/METADATA,sha256=kxo4EtdyoAS0a2Y5jYhQ2cY_HT6P6jV84jUDNdZW_IU,9634
|
|
29
|
+
astris_python-0.1.0.dist-info/RECORD,,
|