astris-python 0.1.0__tar.gz → 0.1.2__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.
- {astris_python-0.1.0 → astris_python-0.1.2}/PKG-INFO +13 -6
- {astris_python-0.1.0 → astris_python-0.1.2}/README.md +4 -1
- {astris_python-0.1.0 → astris_python-0.1.2}/pyproject.toml +11 -5
- {astris_python-0.1.0 → astris_python-0.1.2}/pyproject.toml.orig +11 -5
- astris_python-0.1.2/src/astris/.agents/skills/astris/SKILL.md +342 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/__init__.py +3 -0
- astris_python-0.1.2/src/astris/auth/installer.py +1281 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/cli.py +97 -31
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/config.py +13 -1
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/database/__init__.py +4 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/inertia/response.py +5 -3
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/installer.py +149 -31
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/kernel.py +67 -2
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/routing/__init__.py +2 -0
- astris_python-0.1.2/src/astris/routing/router.py +57 -0
- astris_python-0.1.0/src/astris/auth/installer.py +0 -512
- astris_python-0.1.0/src/astris/routing/router.py +0 -38
- {astris_python-0.1.0 → astris_python-0.1.2}/LICENSE +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/assets/favicon.ico +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/auth/__init__.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/auth/session.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/database/migrations.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/database/session.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/http/__init__.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/http/static.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/inertia/__init__.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/inertia/exceptions.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/inertia/shared.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/inertia/vite.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/py.typed +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/security/__init__.py +0 -0
- {astris_python-0.1.0 → astris_python-0.1.2}/src/astris/security/csrf.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astris-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
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
5
|
Author: Felix Gomez
|
|
6
6
|
Author-email: Felix Gomez <felix@felixgomez.com>
|
|
@@ -32,20 +32,24 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
32
32
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
33
33
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
34
34
|
Classifier: Typing :: Typed
|
|
35
|
-
Requires-Dist: alembic>=1.
|
|
35
|
+
Requires-Dist: alembic>=1.20.0
|
|
36
36
|
Requires-Dist: fastapi>=0.141.1
|
|
37
37
|
Requires-Dist: itsdangerous>=2.2.0
|
|
38
38
|
Requires-Dist: pwdlib[argon2]>=0.3.1
|
|
39
|
-
Requires-Dist: pydantic>=2.13.
|
|
39
|
+
Requires-Dist: pydantic>=2.13.5
|
|
40
40
|
Requires-Dist: pydantic-settings>=2.15.0
|
|
41
|
-
Requires-Dist: sqlmodel>=0.0.
|
|
42
|
-
Requires-Dist: typer>=0.27.
|
|
41
|
+
Requires-Dist: sqlmodel>=0.0.42
|
|
42
|
+
Requires-Dist: typer>=0.27.2
|
|
43
43
|
Requires-Dist: uvicorn[standard]>=0.52.4
|
|
44
44
|
Requires-Python: >=3.11
|
|
45
|
+
Project-URL: Homepage, https://astris.dev
|
|
46
|
+
Project-URL: Documentation, https://astris.dev
|
|
47
|
+
Project-URL: Repository, https://github.com/TheFelixGomez/astris
|
|
48
|
+
Project-URL: Issues, https://github.com/TheFelixGomez/astris/issues
|
|
45
49
|
Description-Content-Type: text/markdown
|
|
46
50
|
|
|
47
51
|
<p align="center" style="padding: 20px 0 10px 0;">
|
|
48
|
-
<a href="https://
|
|
52
|
+
<a href="https://astris.dev">
|
|
49
53
|
<img src="https://raw.githubusercontent.com/TheFelixGomez/astris/main/.github/assets/astris-logo-name.png" alt="Astris" width="380">
|
|
50
54
|
</a>
|
|
51
55
|
</p>
|
|
@@ -56,6 +60,7 @@ Description-Content-Type: text/markdown
|
|
|
56
60
|
</p>
|
|
57
61
|
|
|
58
62
|
<p align="center">
|
|
63
|
+
<a href="https://astris.dev"><img src="https://img.shields.io/badge/docs-astris.dev-0ea5e9.svg" alt="Documentation"></a>
|
|
59
64
|
<a href="https://pypi.org/project/astris-python/"><img src="https://img.shields.io/pypi/v/astris-python.svg" alt="PyPI version"></a>
|
|
60
65
|
<a href="https://pypi.org/project/astris-python/"><img src="https://img.shields.io/pypi/pyversions/astris-python.svg" alt="Python Versions"></a>
|
|
61
66
|
<a href="https://github.com/TheFelixGomez/astris/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
@@ -71,6 +76,8 @@ Description-Content-Type: text/markdown
|
|
|
71
76
|
|
|
72
77
|
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
78
|
|
|
79
|
+
📖 **Explore the full documentation, guides, and tutorials at [astris.dev](https://astris.dev).**
|
|
80
|
+
|
|
74
81
|
---
|
|
75
82
|
|
|
76
83
|
## 🌟 Key Features
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center" style="padding: 20px 0 10px 0;">
|
|
2
|
-
<a href="https://
|
|
2
|
+
<a href="https://astris.dev">
|
|
3
3
|
<img src="https://raw.githubusercontent.com/TheFelixGomez/astris/main/.github/assets/astris-logo-name.png" alt="Astris" width="380">
|
|
4
4
|
</a>
|
|
5
5
|
</p>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
+
<a href="https://astris.dev"><img src="https://img.shields.io/badge/docs-astris.dev-0ea5e9.svg" alt="Documentation"></a>
|
|
13
14
|
<a href="https://pypi.org/project/astris-python/"><img src="https://img.shields.io/pypi/v/astris-python.svg" alt="PyPI version"></a>
|
|
14
15
|
<a href="https://pypi.org/project/astris-python/"><img src="https://img.shields.io/pypi/pyversions/astris-python.svg" alt="Python Versions"></a>
|
|
15
16
|
<a href="https://github.com/TheFelixGomez/astris/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
@@ -25,6 +26,8 @@
|
|
|
25
26
|
|
|
26
27
|
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.
|
|
27
28
|
|
|
29
|
+
📖 **Explore the full documentation, guides, and tutorials at [astris.dev](https://astris.dev).**
|
|
30
|
+
|
|
28
31
|
---
|
|
29
32
|
|
|
30
33
|
## 🌟 Key Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "astris-python"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "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
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -35,14 +35,14 @@ classifiers = [
|
|
|
35
35
|
"Typing :: Typed",
|
|
36
36
|
]
|
|
37
37
|
dependencies = [
|
|
38
|
-
"alembic>=1.
|
|
38
|
+
"alembic>=1.20.0",
|
|
39
39
|
"fastapi>=0.141.1",
|
|
40
40
|
"itsdangerous>=2.2.0",
|
|
41
41
|
"pwdlib[argon2]>=0.3.1",
|
|
42
|
-
"pydantic>=2.13.
|
|
42
|
+
"pydantic>=2.13.5",
|
|
43
43
|
"pydantic-settings>=2.15.0",
|
|
44
|
-
"sqlmodel>=0.0.
|
|
45
|
-
"typer>=0.27.
|
|
44
|
+
"sqlmodel>=0.0.42",
|
|
45
|
+
"typer>=0.27.2",
|
|
46
46
|
"uvicorn[standard]>=0.52.4",
|
|
47
47
|
]
|
|
48
48
|
|
|
@@ -50,6 +50,12 @@ dependencies = [
|
|
|
50
50
|
name = "Felix Gomez"
|
|
51
51
|
email = "felix@felixgomez.com"
|
|
52
52
|
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://astris.dev"
|
|
55
|
+
Documentation = "https://astris.dev"
|
|
56
|
+
Repository = "https://github.com/TheFelixGomez/astris"
|
|
57
|
+
Issues = "https://github.com/TheFelixGomez/astris/issues"
|
|
58
|
+
|
|
53
59
|
[project.scripts]
|
|
54
60
|
astris = "astris.installer:installer_cli"
|
|
55
61
|
astris-python = "astris.installer:installer_cli"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "astris-python"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "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
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -38,17 +38,23 @@ classifiers = [
|
|
|
38
38
|
"Typing :: Typed",
|
|
39
39
|
]
|
|
40
40
|
dependencies = [
|
|
41
|
-
"alembic>=1.
|
|
41
|
+
"alembic>=1.20.0",
|
|
42
42
|
"fastapi>=0.141.1",
|
|
43
43
|
"itsdangerous>=2.2.0",
|
|
44
44
|
"pwdlib[argon2]>=0.3.1",
|
|
45
|
-
"pydantic>=2.13.
|
|
45
|
+
"pydantic>=2.13.5",
|
|
46
46
|
"pydantic-settings>=2.15.0",
|
|
47
|
-
"sqlmodel>=0.0.
|
|
48
|
-
"typer>=0.27.
|
|
47
|
+
"sqlmodel>=0.0.42",
|
|
48
|
+
"typer>=0.27.2",
|
|
49
49
|
"uvicorn[standard]>=0.52.4",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://astris.dev"
|
|
54
|
+
Documentation = "https://astris.dev"
|
|
55
|
+
Repository = "https://github.com/TheFelixGomez/astris"
|
|
56
|
+
Issues = "https://github.com/TheFelixGomez/astris/issues"
|
|
57
|
+
|
|
52
58
|
[project.scripts]
|
|
53
59
|
astris = "astris.installer:installer_cli"
|
|
54
60
|
astris-python = "astris.installer:installer_cli"
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: astris
|
|
3
|
+
description: Astris best practices and conventions. Use when working with Astris full-stack Python applications, controllers, Inertia.js Vue 3 views, SQLModel databases, Orbit CLI, and security. Keeps Astris code clean and up to date with the latest features and patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Astris
|
|
7
|
+
|
|
8
|
+
Official Astris skill to write code with best practices, keeping up to date with new versions and features.
|
|
9
|
+
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
* Application entrypoint: `app = Astris()` in `main.py`; see [Application Kernel](#application-kernel).
|
|
13
|
+
* Configuration: manage via `.env` and `app/core/config.py`; see [Configuration](#configuration-appcoreconfigpy).
|
|
14
|
+
* Controllers: define in `app/modules/<name>/<name>_controller.py` with `Controller`; see [Controllers & Routing](#controllers--routing).
|
|
15
|
+
* Views: return `InertiaResponse(request, "Component/Path", props={...})`; see [Inertia.js Monolith](#inertiajs-monolith-vue-3--tailwind-css-v4).
|
|
16
|
+
* Mutations: return `RedirectResponse(url="/...", status_code=status.HTTP_303_SEE_OTHER)` for POST/PUT/PATCH/DELETE; see [Form Handling & Mutations](#form-handling--mutations).
|
|
17
|
+
* Database sessions: inject `DatabaseSession` into services via `Depends()`; see [Database & Services](#database--services).
|
|
18
|
+
* Models: define in `app/modules/<name>/<name>_model.py` with `SQLModel`; see [SQLModel Models](#sqlmodel-models).
|
|
19
|
+
* Flash messages: use `flash(request, "type", "message")`; see [Flash Messages & Shared Data](#flash-messages--shared-data).
|
|
20
|
+
* Authentication: use `auth_required` / `guest_required` route dependencies and `AuthUser`; see [Authentication & Security](#authentication--security).
|
|
21
|
+
* Orbit CLI: use `uv run orbit <command>`; see [Orbit CLI](#orbit-cli).
|
|
22
|
+
|
|
23
|
+
## Application Kernel
|
|
24
|
+
|
|
25
|
+
Instantiate `Astris()` in `main.py`:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from astris import Astris
|
|
29
|
+
|
|
30
|
+
app = Astris()
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The kernel automatically:
|
|
34
|
+
1. Loads settings from `app/core/config.py` and `.env`.
|
|
35
|
+
2. Registers security middlewares (CORS, PublicStatic, Session, Flash, CSRF).
|
|
36
|
+
3. Auto-discovers and attaches all controllers in `app/modules/`.
|
|
37
|
+
4. Binds the Inertia.js view engine to `resources/views/root.html`.
|
|
38
|
+
|
|
39
|
+
## Configuration (`app/core/config.py`)
|
|
40
|
+
|
|
41
|
+
Astris generates `app/core/config.py` extending the base `Settings` class with automatic `.env` type-casting:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from astris.config import Settings as BaseAppSettings
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Settings(BaseAppSettings):
|
|
48
|
+
"""Extend application settings with custom environment variables."""
|
|
49
|
+
|
|
50
|
+
stripe_api_key: str | None = None
|
|
51
|
+
redis_url: str = "redis://localhost:6379"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
settings = Settings()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Access strongly typed settings anywhere in your app:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from app.core.config import settings
|
|
61
|
+
|
|
62
|
+
api_key = settings.stripe_api_key
|
|
63
|
+
db_url = settings.database_url
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Controllers & Routing
|
|
67
|
+
|
|
68
|
+
Place controllers inside `app/modules/<module>/<module>_controller.py`. Astris automatically discovers any `Controller` in files matching `*_controller.py`. `Controller` is a direct subclass of FastAPI's `APIRouter`.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from astris.routing import Controller, Depends, Path, Query
|
|
72
|
+
from astris.http import Request
|
|
73
|
+
from astris.inertia import InertiaResponse
|
|
74
|
+
from app.modules.billing.billing_service import BillingService
|
|
75
|
+
|
|
76
|
+
controller = Controller(prefix="/billing", tags=["Billing"])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@controller.get("/")
|
|
80
|
+
async def index(
|
|
81
|
+
request: Request,
|
|
82
|
+
billing: BillingService = Depends(),
|
|
83
|
+
) -> InertiaResponse:
|
|
84
|
+
invoices = billing.get_recent_invoices()
|
|
85
|
+
return InertiaResponse(
|
|
86
|
+
request,
|
|
87
|
+
"Billing/Index",
|
|
88
|
+
props={"invoices": [inv.model_dump() for inv in invoices]},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@controller.get("/{invoice_id}")
|
|
93
|
+
async def show(
|
|
94
|
+
request: Request,
|
|
95
|
+
invoice_id: int,
|
|
96
|
+
billing: BillingService = Depends(),
|
|
97
|
+
) -> InertiaResponse:
|
|
98
|
+
invoice = billing.get_invoice(invoice_id)
|
|
99
|
+
return InertiaResponse(
|
|
100
|
+
request,
|
|
101
|
+
"Billing/Show",
|
|
102
|
+
props={"invoice": invoice.model_dump()},
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Form Handling & Mutations
|
|
107
|
+
|
|
108
|
+
Inertia requires that mutating requests (`POST`, `PUT`, `PATCH`, `DELETE`) redirect with a `303 See Other` status code.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from astris.routing import Controller, Depends
|
|
112
|
+
from astris.http import Request, RedirectResponse, status
|
|
113
|
+
from astris.inertia import flash
|
|
114
|
+
from app.modules.billing.billing_service import BillingService
|
|
115
|
+
from app.modules.billing.billing_model import InvoiceCreate
|
|
116
|
+
|
|
117
|
+
controller = Controller(prefix="/billing", tags=["Billing"])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@controller.post("/")
|
|
121
|
+
async def store(
|
|
122
|
+
request: Request,
|
|
123
|
+
dto: InvoiceCreate,
|
|
124
|
+
billing: BillingService = Depends(),
|
|
125
|
+
) -> RedirectResponse:
|
|
126
|
+
invoice = billing.create_invoice(dto)
|
|
127
|
+
flash(request, "success", f"Invoice #{invoice.id} created successfully!")
|
|
128
|
+
return RedirectResponse(url="/billing", status_code=status.HTTP_303_SEE_OTHER)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@controller.delete("/{invoice_id}")
|
|
132
|
+
async def destroy(
|
|
133
|
+
request: Request,
|
|
134
|
+
invoice_id: int,
|
|
135
|
+
billing: BillingService = Depends(),
|
|
136
|
+
) -> RedirectResponse:
|
|
137
|
+
billing.delete_invoice(invoice_id)
|
|
138
|
+
flash(request, "success", "Invoice deleted successfully.")
|
|
139
|
+
return RedirectResponse(url="/billing", status_code=status.HTTP_303_SEE_OTHER)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Flash Messages & Shared Data
|
|
143
|
+
|
|
144
|
+
* Flash messages set with `flash(request, "success", "...")` are automatically shared with the frontend in `$page.props.flash`.
|
|
145
|
+
* Global props can be shared per-request using `share(request, "key", value)`.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from astris.inertia import flash, share
|
|
149
|
+
|
|
150
|
+
flash(request, "error", "Invalid payment credentials.")
|
|
151
|
+
share(request, "team_name", "Acme Corp")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Database & Services
|
|
155
|
+
|
|
156
|
+
### 1. Service Layer with Injected Session
|
|
157
|
+
Accept `session: DatabaseSession` in the constructor:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from astris.database import DatabaseSession, select
|
|
161
|
+
from app.modules.billing.billing_model import Invoice, InvoiceCreate
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class BillingService:
|
|
165
|
+
def __init__(self, session: DatabaseSession):
|
|
166
|
+
self.session = session
|
|
167
|
+
|
|
168
|
+
def get_recent_invoices(self) -> list[Invoice]:
|
|
169
|
+
return self.session.exec(select(Invoice).order_by(Invoice.id.desc())).all()
|
|
170
|
+
|
|
171
|
+
def get_invoice(self, invoice_id: int) -> Invoice:
|
|
172
|
+
return self.session.get(Invoice, invoice_id)
|
|
173
|
+
|
|
174
|
+
def create_invoice(self, dto: InvoiceCreate) -> Invoice:
|
|
175
|
+
invoice = Invoice.model_validate(dto)
|
|
176
|
+
self.session.add(invoice)
|
|
177
|
+
self.session.commit()
|
|
178
|
+
self.session.refresh(invoice)
|
|
179
|
+
return invoice
|
|
180
|
+
|
|
181
|
+
def delete_invoice(self, invoice_id: int) -> None:
|
|
182
|
+
invoice = self.get_invoice(invoice_id)
|
|
183
|
+
if invoice:
|
|
184
|
+
self.session.delete(invoice)
|
|
185
|
+
self.session.commit()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 2. Standalone Scripts & Background Tasks
|
|
189
|
+
Use `with db.session() as session:` only outside HTTP requests (in seeders, CLI scripts, or background workers):
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from astris.database import db, select
|
|
193
|
+
from app.modules.billing.billing_model import Invoice
|
|
194
|
+
|
|
195
|
+
with db.session() as session:
|
|
196
|
+
invoices = session.exec(select(Invoice)).all()
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## SQLModel Models
|
|
200
|
+
|
|
201
|
+
Define models in `app/modules/<module>/<module>_model.py`. Astris auto-imports models to register table metadata with Alembic:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from astris.database import Field, SQLModel
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class InvoiceBase(SQLModel):
|
|
208
|
+
customer_name: str = Field(index=True)
|
|
209
|
+
amount: float
|
|
210
|
+
status: str = Field(default="pending")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Invoice(InvoiceBase, table=True):
|
|
214
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class InvoiceCreate(InvoiceBase):
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class InvoicePublic(InvoiceBase):
|
|
222
|
+
id: int
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Inertia.js Monolith (Vue 3 + Tailwind CSS v4)
|
|
226
|
+
|
|
227
|
+
Place page components in `resources/js/Pages/<Module>/<Page>.vue`. Always use `<script setup lang="ts">`:
|
|
228
|
+
|
|
229
|
+
```vue
|
|
230
|
+
<script setup lang="ts">
|
|
231
|
+
import { useForm, Head, Link, usePage } from '@inertiajs/vue3'
|
|
232
|
+
|
|
233
|
+
interface Invoice {
|
|
234
|
+
id: number
|
|
235
|
+
customer_name: string
|
|
236
|
+
amount: number
|
|
237
|
+
status: string
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
defineProps<{
|
|
241
|
+
invoices: Invoice[]
|
|
242
|
+
}>()
|
|
243
|
+
|
|
244
|
+
const page = usePage()
|
|
245
|
+
|
|
246
|
+
const form = useForm({
|
|
247
|
+
customer_name: '',
|
|
248
|
+
amount: 0,
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const submit = () => {
|
|
252
|
+
form.post('/billing', {
|
|
253
|
+
onSuccess: () => form.reset(),
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
</script>
|
|
257
|
+
|
|
258
|
+
<template>
|
|
259
|
+
<Head title="Billing" />
|
|
260
|
+
<div class="max-w-4xl mx-auto p-6 space-y-6">
|
|
261
|
+
<!-- Flash Banner -->
|
|
262
|
+
<div
|
|
263
|
+
v-if="page.props.flash?.success"
|
|
264
|
+
class="p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 text-sm"
|
|
265
|
+
>
|
|
266
|
+
{{ page.props.flash.success }}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<h1 class="text-2xl font-bold text-slate-900 dark:text-white">Invoices</h1>
|
|
270
|
+
|
|
271
|
+
<!-- Form with Automatic Server Validation Error Bindings -->
|
|
272
|
+
<form @submit.prevent="submit" class="space-y-4">
|
|
273
|
+
<div>
|
|
274
|
+
<label class="block text-sm font-medium">Customer Name</label>
|
|
275
|
+
<input
|
|
276
|
+
v-model="form.customer_name"
|
|
277
|
+
type="text"
|
|
278
|
+
class="w-full px-3 py-2 border rounded-lg"
|
|
279
|
+
:class="{ 'border-rose-500': form.errors.customer_name }"
|
|
280
|
+
/>
|
|
281
|
+
<p v-if="form.errors.customer_name" class="text-sm text-rose-500 mt-1">
|
|
282
|
+
{{ form.errors.customer_name }}
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<button
|
|
287
|
+
type="submit"
|
|
288
|
+
:disabled="form.processing"
|
|
289
|
+
class="px-4 py-2 bg-sky-500 text-white rounded-lg disabled:opacity-50"
|
|
290
|
+
>
|
|
291
|
+
Create Invoice
|
|
292
|
+
</button>
|
|
293
|
+
</form>
|
|
294
|
+
</div>
|
|
295
|
+
</template>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Authentication & Security
|
|
299
|
+
|
|
300
|
+
1. **Guards on Controllers**:
|
|
301
|
+
```python
|
|
302
|
+
from astris.routing import Controller
|
|
303
|
+
from astris.auth import auth_required, guest_required, AuthUser
|
|
304
|
+
from astris.http import Request
|
|
305
|
+
from astris.inertia import InertiaResponse
|
|
306
|
+
|
|
307
|
+
controller = Controller(prefix="/dashboard", dependencies=[auth_required])
|
|
308
|
+
|
|
309
|
+
@controller.get("/")
|
|
310
|
+
async def dashboard(request: Request, user: AuthUser) -> InertiaResponse:
|
|
311
|
+
return InertiaResponse(request, "Dashboard", props={"user": user})
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
2. **Session Authentication Helpers**:
|
|
315
|
+
* `login_user(request, user_id)`: Log a user in by storing signed session cookie.
|
|
316
|
+
* `logout_user(request)`: Terminate current session.
|
|
317
|
+
* `hash_password(password)` / `verify_password(plain, hashed)`: Secure Argon2id password management.
|
|
318
|
+
|
|
319
|
+
3. **CSRF Protection**:
|
|
320
|
+
Astris sets the `XSRF-TOKEN` cookie automatically. Inertia and Axios send the `X-XSRF-TOKEN` header on all mutating requests with zero frontend configuration.
|
|
321
|
+
|
|
322
|
+
## Orbit CLI
|
|
323
|
+
|
|
324
|
+
Always run Orbit commands via `uv run orbit`:
|
|
325
|
+
|
|
326
|
+
* `uv run orbit serve`: Start local development server (Uvicorn backend + Vite frontend HMR concurrently).
|
|
327
|
+
* `uv run orbit serve --prod`: Start production server on `0.0.0.0` with multi-worker processes.
|
|
328
|
+
* `uv run orbit make:module <name>`: Scaffold domain module (`controller`, `service`, `model`).
|
|
329
|
+
* `uv run orbit make:controller <name> [--module <mod>]`: Generate a controller.
|
|
330
|
+
* `uv run orbit make:model <name> [--module <mod>]`: Generate a SQLModel database table.
|
|
331
|
+
* `uv run orbit make:migration "<message>"`: Autogenerate Alembic schema migration from models.
|
|
332
|
+
* `uv run orbit migrate`: Execute pending migrations.
|
|
333
|
+
* `uv run orbit migrate:rollback`: Roll back database migrations.
|
|
334
|
+
* `uv run orbit migrate:status`: Check current migration revisions.
|
|
335
|
+
* `uv run orbit key:generate`: Generate a 32-byte secret key and update `APP_KEY` in `.env`.
|
|
336
|
+
|
|
337
|
+
## Key Rules
|
|
338
|
+
|
|
339
|
+
* Always configure via `.env` and `app/core/config.py`.
|
|
340
|
+
* Never hardcode secrets in code.
|
|
341
|
+
* Always use `DatabaseSession` with `Depends()` in route handlers and services.
|
|
342
|
+
* Always redirect with `status.HTTP_303_SEE_OTHER` after POST/PUT/PATCH/DELETE mutations.
|