astris-python 0.1.0__tar.gz → 0.1.1__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.
Files changed (30) hide show
  1. {astris_python-0.1.0 → astris_python-0.1.1}/PKG-INFO +12 -5
  2. {astris_python-0.1.0 → astris_python-0.1.1}/README.md +4 -1
  3. {astris_python-0.1.0 → astris_python-0.1.1}/pyproject.toml +10 -4
  4. {astris_python-0.1.0 → astris_python-0.1.1}/pyproject.toml.orig +10 -4
  5. astris_python-0.1.1/src/astris/.agents/skills/astris/SKILL.md +342 -0
  6. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/__init__.py +3 -0
  7. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/cli.py +77 -20
  8. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/config.py +13 -1
  9. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/installer.py +93 -8
  10. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/kernel.py +1 -1
  11. {astris_python-0.1.0 → astris_python-0.1.1}/LICENSE +0 -0
  12. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/assets/favicon.ico +0 -0
  13. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/auth/__init__.py +0 -0
  14. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/auth/installer.py +0 -0
  15. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/auth/session.py +0 -0
  16. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/database/__init__.py +0 -0
  17. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/database/migrations.py +0 -0
  18. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/database/session.py +0 -0
  19. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/http/__init__.py +0 -0
  20. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/http/static.py +0 -0
  21. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/inertia/__init__.py +0 -0
  22. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/inertia/exceptions.py +0 -0
  23. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/inertia/response.py +0 -0
  24. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/inertia/shared.py +0 -0
  25. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/inertia/vite.py +0 -0
  26. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/py.typed +0 -0
  27. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/routing/__init__.py +0 -0
  28. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/routing/router.py +0 -0
  29. {astris_python-0.1.0 → astris_python-0.1.1}/src/astris/security/__init__.py +0 -0
  30. {astris_python-0.1.0 → astris_python-0.1.1}/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.0
3
+ Version: 0.1.1
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>
@@ -36,16 +36,20 @@ Requires-Dist: alembic>=1.19.1
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.4
39
+ Requires-Dist: pydantic>=2.13.5
40
40
  Requires-Dist: pydantic-settings>=2.15.0
41
- Requires-Dist: sqlmodel>=0.0.39
42
- Requires-Dist: typer>=0.27.1
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://github.com/TheFelixGomez/astris">
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://github.com/TheFelixGomez/astris">
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.0"
3
+ version = "0.1.1"
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"
@@ -39,10 +39,10 @@ dependencies = [
39
39
  "fastapi>=0.141.1",
40
40
  "itsdangerous>=2.2.0",
41
41
  "pwdlib[argon2]>=0.3.1",
42
- "pydantic>=2.13.4",
42
+ "pydantic>=2.13.5",
43
43
  "pydantic-settings>=2.15.0",
44
- "sqlmodel>=0.0.39",
45
- "typer>=0.27.1",
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.0"
3
+ version = "0.1.1"
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 = [
@@ -42,13 +42,19 @@ dependencies = [
42
42
  "fastapi>=0.141.1",
43
43
  "itsdangerous>=2.2.0",
44
44
  "pwdlib[argon2]>=0.3.1",
45
- "pydantic>=2.13.4",
45
+ "pydantic>=2.13.5",
46
46
  "pydantic-settings>=2.15.0",
47
- "sqlmodel>=0.0.39",
48
- "typer>=0.27.1",
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.
@@ -1,7 +1,10 @@
1
1
  from astris.config import Settings
2
2
  from astris.kernel import Astris
3
3
 
4
+ __version__ = "0.1.1"
5
+
4
6
  __all__ = [
5
7
  "Astris",
6
8
  "Settings",
9
+ "__version__",
7
10
  ]
@@ -31,20 +31,36 @@ def list_commands(ctx: typer.Context):
31
31
 
32
32
  @orbit_cli.command()
33
33
  def serve(
34
- host: str = typer.Option(
35
- "127.0.0.1", "--host", "-h", help="Bind socket to this host"
34
+ host: str | None = typer.Option(
35
+ None, "--host", "-h", help="Bind socket to this host (defaults to 127.0.0.1 in dev, 0.0.0.0 in prod)"
36
36
  ),
37
37
  port: int = typer.Option(8000, "--port", "-p", help="Bind socket to this port"),
38
- reload: bool = typer.Option(
39
- True, "--reload/--no-reload", help="Enable/disable auto-reload"
38
+ reload: bool | None = typer.Option(
39
+ None, "--reload/--no-reload", help="Enable/disable auto-reload"
40
40
  ),
41
- vite: bool = typer.Option(
42
- True,
41
+ vite: bool | None = typer.Option(
42
+ None,
43
43
  "--vite/--no-vite",
44
44
  help="Start concurrent Vite dev server if package.json exists",
45
45
  ),
46
+ prod: bool = typer.Option(
47
+ False,
48
+ "--prod",
49
+ help="Run in production mode (disables reload and Vite, sets host to 0.0.0.0, enables multi-worker)",
50
+ ),
51
+ workers: int | None = typer.Option(
52
+ None,
53
+ "--workers",
54
+ "-w",
55
+ help="Number of worker processes for production (defaults to 4 in prod)",
56
+ ),
46
57
  ):
47
- """Start the development server (with concurrent Vite dev server if package.json exists)."""
58
+ """Start the application server (development with Vite HMR or production with --prod)."""
59
+ resolved_host = host or ("0.0.0.0" if prod else "127.0.0.1")
60
+ resolved_reload = False if prod else (True if reload is None else reload)
61
+ resolved_vite = False if prod else (True if vite is None else vite)
62
+ resolved_workers = workers if workers is not None else (4 if prod else 1)
63
+
48
64
  cwd = Path.cwd()
49
65
  cwd_str = str(cwd)
50
66
  if cwd_str not in sys.path:
@@ -54,7 +70,7 @@ def serve(
54
70
  package_json = cwd / "package.json"
55
71
  vite_proc = None
56
72
 
57
- if package_json.exists() and vite:
73
+ if package_json.exists() and resolved_vite:
58
74
  typer.secho(
59
75
  "⚡ Full-stack project detected. Starting Vite dev server...",
60
76
  fg=typer.colors.MAGENTA,
@@ -73,26 +89,30 @@ def serve(
73
89
  fg=typer.colors.YELLOW,
74
90
  )
75
91
 
92
+ mode_label = "in production mode" if prod else ""
76
93
  typer.secho(
77
- f"🚀 Astris entering orbit on http://{host}:{port}", fg=typer.colors.CYAN
94
+ f"🚀 Astris entering orbit {mode_label} on http://{resolved_host}:{port}",
95
+ fg=typer.colors.CYAN,
78
96
  )
79
97
 
80
98
  try:
81
99
  reload_dirs = []
82
- if (cwd / "app").exists():
83
- reload_dirs.append(str(cwd / "app"))
84
- if (cwd / "database").exists():
85
- reload_dirs.append(str(cwd / "database"))
86
- if not reload_dirs:
87
- reload_dirs = [cwd_str]
100
+ if resolved_reload:
101
+ if (cwd / "app").exists():
102
+ reload_dirs.append(str(cwd / "app"))
103
+ if (cwd / "database").exists():
104
+ reload_dirs.append(str(cwd / "database"))
105
+ if not reload_dirs:
106
+ reload_dirs = [cwd_str]
88
107
 
89
108
  uvicorn.run(
90
109
  "main:app",
91
- host=host,
110
+ host=resolved_host,
92
111
  port=port,
93
- reload=reload,
94
- reload_dirs=reload_dirs if reload else None,
95
- reload_includes=["*.py", ".env*"] if reload else None,
112
+ reload=resolved_reload,
113
+ workers=resolved_workers if not resolved_reload else None,
114
+ reload_dirs=reload_dirs if resolved_reload else None,
115
+ reload_includes=["*.py", ".env*"] if resolved_reload else None,
96
116
  reload_excludes=[
97
117
  "node_modules",
98
118
  "resources",
@@ -102,7 +122,7 @@ def serve(
102
122
  "dist",
103
123
  "build",
104
124
  ]
105
- if reload
125
+ if resolved_reload
106
126
  else None,
107
127
  app_dir=cwd_str,
108
128
  )
@@ -419,3 +439,40 @@ def make_auth():
419
439
  except Exception as e:
420
440
  typer.secho(f"Error generating auth scaffolding: {e}", fg=typer.colors.RED)
421
441
  raise typer.Exit(1) from e
442
+
443
+
444
+ @orbit_cli.command("skills:install")
445
+ def skills_install(
446
+ claude: bool = typer.Option(
447
+ False, "--claude", help="Install skills for Claude Code in .claude/skills as well"
448
+ ),
449
+ ):
450
+ """Install or regenerate AI agent skills for the project."""
451
+ astris_skill = Path(__file__).parent / ".agents" / "skills" / "astris" / "SKILL.md"
452
+ if not astris_skill.exists():
453
+ typer.secho("Error: Astris skill file not found in package.", fg=typer.colors.RED)
454
+ raise typer.Exit(1)
455
+
456
+ skill_text = astris_skill.read_text(encoding="utf-8")
457
+ targets = [Path.cwd() / ".agents" / "skills" / "astris"]
458
+ if claude:
459
+ targets.append(Path.cwd() / ".claude" / "skills" / "astris")
460
+
461
+ for target_dir in targets:
462
+ target_dir.mkdir(parents=True, exist_ok=True)
463
+ (target_dir / "SKILL.md").write_text(skill_text, encoding="utf-8")
464
+
465
+ typer.secho("✓ Astris AI skill installed successfully!", fg=typer.colors.GREEN)
466
+ typer.echo(" - Universal: .agents/skills/astris/SKILL.md")
467
+ if claude:
468
+ typer.echo(" - Claude: .claude/skills/astris/SKILL.md")
469
+
470
+
471
+ @orbit_cli.command("skills:update")
472
+ def skills_update(
473
+ claude: bool = typer.Option(
474
+ False, "--claude", help="Update skills for Claude Code in .claude/skills as well"
475
+ ),
476
+ ):
477
+ """Update and re-sync all AI agent skills to match installed package versions."""
478
+ skills_install(claude=claude)
@@ -28,9 +28,21 @@ class Settings(BaseSettings):
28
28
  # Sessions & Security
29
29
  session_cookie_name: str = "astris_session"
30
30
  session_max_age: int | None = 14 * 24 * 60 * 60 # 14 days
31
- session_https_only: bool = False
31
+ session_https_only: bool | None = None
32
32
  session_same_site: Literal["lax", "strict", "none"] = "lax"
33
33
 
34
+ @property
35
+ def is_production(self) -> bool:
36
+ """Return True if running in production mode."""
37
+ return self.app_env.lower() in ("production", "prod")
38
+
39
+ @property
40
+ def resolved_session_https_only(self) -> bool:
41
+ """Auto-enable HTTPS-only cookies in production unless explicitly overridden."""
42
+ if self.session_https_only is not None:
43
+ return self.session_https_only
44
+ return self.is_production
45
+
34
46
  # CORS & CSRF
35
47
  cors_origins: list[str] = [
36
48
  "http://localhost:5173",
@@ -36,6 +36,11 @@ def new(
36
36
  "--auth/--no-auth",
37
37
  help="Scaffold full-stack authentication starter kit (default: enabled)",
38
38
  ),
39
+ claude: bool = typer.Option(
40
+ False,
41
+ "--claude",
42
+ help="Scaffold Claude Code agent skills in .claude/skills as well",
43
+ ),
39
44
  ):
40
45
  """Scaffold a brand-new Astris full-stack project."""
41
46
  project_dir = Path.cwd() / name
@@ -99,13 +104,31 @@ def new(
99
104
  ensure_migration_setup(project_dir)
100
105
 
101
106
  # 1b. Centralized settings: app/core/config.py
102
- config_stub = """from astris.config import Settings as BaseAppSettings
107
+ config_stub = """\"\"\"
108
+ |--------------------------------------------------------------------------
109
+ | Application Configuration
110
+ |--------------------------------------------------------------------------
111
+ |
112
+ | Centralized, type-safe application settings powered by Pydantic Settings.
113
+ | Environment variables defined in your .env file are automatically
114
+ | validated and type-cast when the application boots.
115
+ |
116
+ | Access settings anywhere in your app:
117
+ | from app.core.config import settings
118
+ | api_key = settings.stripe_api_key
119
+ |
120
+ \"\"\"
121
+
122
+ from astris.config import Settings as BaseAppSettings
103
123
 
104
124
 
105
125
  class Settings(BaseAppSettings):
106
126
  \"\"\"Extend application settings with custom environment variables.\"\"\"
107
127
 
108
- # Add custom settings here (e.g. STRIPE_KEY, REDIS_URL, etc.)
128
+ # Define custom environment variables below:
129
+ # stripe_api_key: str | None = None
130
+ # redis_url: str = "redis://localhost:6379"
131
+ # max_upload_size_mb: int = 25
109
132
  pass
110
133
 
111
134
 
@@ -145,7 +168,7 @@ dependencies = []
145
168
  '''
146
169
  (project_dir / "pyproject.toml").write_text(pyproject_content, encoding="utf-8")
147
170
  project_readme = f"""<p align="center" style="padding: 20px 0 10px 0;">
148
- <a href="https://github.com/TheFelixGomez/astris">
171
+ <a href="https://astris.dev">
149
172
  <img src="https://raw.githubusercontent.com/TheFelixGomez/astris/main/.github/assets/astris-logo-name.png" alt="Astris" width="380">
150
173
  </a>
151
174
  </p>
@@ -184,12 +207,26 @@ Open **`http://localhost:8000`** in your browser.
184
207
  | Command | Description |
185
208
  | :--- | :--- |
186
209
  | `uv run orbit serve` | Start full-stack development server with hot-reloading |
187
- | `uv run orbit make:module <name>` | Scaffold a complete domain module (Controller, Model, View) |
210
+ | `uv run orbit make:module <name>` | Scaffold a complete domain module (Controller, Service, Model) |
188
211
  | `uv run orbit make:controller <name>` | Generate an Astris controller |
189
212
  | `uv run orbit make:model <name>` | Generate a database model |
190
213
  | `uv run orbit migrate` | Run all pending database migrations |
191
214
  | `uv run orbit make:migration "<message>"` | Auto-generate a new database schema migration |
192
215
  | `uv run orbit key:generate` | Generate a new 32-byte secret `APP_KEY` in `.env` |
216
+ | `uv run orbit skills:install` | Install or regenerate AI agent skills for the project |
217
+ | `uv run orbit skills:update` | Re-sync AI agent skills to match installed package versions |
218
+
219
+ ---
220
+
221
+ ## 🤖 Building with AI
222
+
223
+ This project comes pre-configured with official Astris skills in `.agents/skills/astris/` (Universal assistants) and `.claude/skills/astris/` (Claude Code).
224
+ To refresh or reinstall skills after updating dependencies:
225
+ ```bash
226
+ uv run orbit skills:install
227
+ # Or for Claude Code:
228
+ uv run orbit skills:install --claude
229
+ ```
193
230
 
194
231
  ---
195
232
 
@@ -200,6 +237,7 @@ Open **`http://localhost:8000`** in your browser.
200
237
  * **`resources/js/Pages/`**: Frontend single-page application views.
201
238
  * **`database/migrations/`**: Database schema migrations managed by Orbit.
202
239
  * **`public/`**: Web root directory for static assets (favicon, images, robots.txt).
240
+ * **`.agents/skills/` & `.claude/skills/`**: Official AI agent skills for Antigravity, Claude Code, Codex, Cursor, and universal assistants.
203
241
 
204
242
  ---
205
243
 
@@ -207,6 +245,7 @@ Open **`http://localhost:8000`** in your browser.
207
245
 
208
246
  **Astris** is a modern full-stack web framework for Python with expressive, type-safe elegance. Designed to help developers build and ship modern web applications with speed and simplicity.
209
247
 
248
+ * **Documentation**: [https://astris.dev](https://astris.dev)
210
249
  * **Repository**: [https://github.com/TheFelixGomez/astris](https://github.com/TheFelixGomez/astris)
211
250
  * **Author**: Felix Gomez ([@TheFelixGomez](https://github.com/TheFelixGomez))
212
251
  """
@@ -217,14 +256,48 @@ Open **`http://localhost:8000`** in your browser.
217
256
  )
218
257
 
219
258
  # 3. main.py entry point
220
- main_content = """from astris import Astris
259
+ main_content = """\"\"\"
260
+ |--------------------------------------------------------------------------
261
+ | Application Entrypoint & Kernel
262
+ |--------------------------------------------------------------------------
263
+ |
264
+ | This file instantiates the Astris application instance.
265
+ | The Astris() kernel automatically:
266
+ | 1. Loads configuration from app/core/config.py and .env
267
+ | 2. Configures the security and session middleware pipeline
268
+ | 3. Discovers domain controllers in app/modules/
269
+ | 4. Connects the Inertia.js Vue 3 frontend view engine
270
+ |
271
+ | Run development server:
272
+ | uv run orbit serve
273
+ |
274
+ | Run in production:
275
+ | uv run orbit serve --prod
276
+ |
277
+ \"\"\"
278
+
279
+ from astris import Astris
221
280
 
222
281
  app = Astris()
223
282
  """
224
283
  (project_dir / "main.py").write_text(main_content, encoding="utf-8")
225
284
 
226
285
  # 4. Default Welcome Controller using InertiaResponse
227
- welcome_controller = """from astris.http import Request
286
+ welcome_controller = """\"\"\"
287
+ |--------------------------------------------------------------------------
288
+ | Welcome Controller
289
+ |--------------------------------------------------------------------------
290
+ |
291
+ | This controller handles the root URL ('/') of your Astris application.
292
+ | It returns an InertiaResponse which renders the Vue 3 component located
293
+ | at resources/js/Pages/Welcome.vue with typed server props.
294
+ |
295
+ | Feel free to modify, replace, or remove this module and controller
296
+ | to suit your application's requirements.
297
+ |
298
+ \"\"\"
299
+
300
+ from astris.http import Request
228
301
  from astris.inertia import InertiaResponse
229
302
  from astris.routing import Controller
230
303
 
@@ -239,10 +312,10 @@ async def index(request: Request) -> InertiaResponse:
239
312
  props={
240
313
  "status": "online",
241
314
  "message": "Welcome to your Astris application! 🚀",
242
- "version": "0.1.0",
315
+ "version": "0.1.1",
243
316
  "api_docs_url": "/docs",
244
317
  "redoc_url": "/redoc",
245
- "docs_url": "https://astris.dev/docs",
318
+ "docs_url": "https://astris.dev",
246
319
  },
247
320
  )
248
321
  """
@@ -586,6 +659,18 @@ const page = usePage();
586
659
  else:
587
660
  subprocess.run(["uv", "add", "astris-python"], cwd=project_dir, check=True)
588
661
 
662
+ # 12. Scaffold bundled Astris agent skills
663
+ astris_skill = Path(__file__).parent / ".agents" / "skills" / "astris" / "SKILL.md"
664
+ if astris_skill.exists():
665
+ skill_text = astris_skill.read_text(encoding="utf-8")
666
+ target_dirs = [project_dir / ".agents" / "skills" / "astris"]
667
+ if claude:
668
+ target_dirs.append(project_dir / ".claude" / "skills" / "astris")
669
+
670
+ for skill_dir_path in target_dirs:
671
+ skill_dir_path.mkdir(parents=True, exist_ok=True)
672
+ (skill_dir_path / "SKILL.md").write_text(skill_text, encoding="utf-8")
673
+
589
674
  typer.secho(
590
675
  f"\n✓ Project {name} created successfully!", fg=typer.colors.GREEN, bold=True
591
676
  )
@@ -79,7 +79,7 @@ class Astris:
79
79
  self.session_https_only = (
80
80
  session_https_only
81
81
  if session_https_only is not None
82
- else self.config.session_https_only
82
+ else self.config.resolved_session_https_only
83
83
  )
84
84
  self.session_same_site = (
85
85
  session_same_site or self.config.session_same_site # type: ignore[assignment]
File without changes