# PyRails Framework Specification

**Version**: 1.0.0
**Target**: Open-source Python web framework inspired by Ruby on Rails
**Philosophy**: Convention over Configuration for FastAPI applications

---

## Executive Summary

PyRails is a Rails-inspired web framework built on FastAPI that brings Ruby on Rails' legendary developer experience to Python. It provides:

- **Rails-like MVC structure** for FastAPI applications
- **Powerful CLI** with commands like `db:migrate`, `db:seed`, `server`, `console`
- **Async-first** design using SQLAlchemy 2.0+ and asyncio
- **Zero-config defaults** with full customization when needed
- **Batteries included** authentication, admin panel, migrations, and seeding

**Core Insight**: Rails proved that convention over configuration dramatically accelerates development. PyRails brings this philosophy to Python's async ecosystem.

---

## Design Philosophy

### 1. Convention Over Configuration

**File locations are predictable:**
```
app/
  models/          # Database models (one file per table)
  controllers/     # HTTP request handlers (one file per resource)
  views/           # Jinja2 templates
  services/        # Business logic (reusable across controllers)
  assets/          # Static files (CSS, JS, images)
config/
  application.py   # Main FastAPI app
  database.py      # Database configuration
db/
  seeds/           # Seed data files
    development.py
    production.py
```

**Naming conventions are enforced:**
- Models: `user.py` (singular) → `class User` → table `users`
- Controllers: `users_controller.py` → router prefix `/api/users`
- Services: `email_service.py` → `class EmailService`
- Views: `dashboard.html`, `login.html`

### 2. CLI-First Developer Experience

**Everything accessible via `pyrails` command:**
```bash
pyrails new myapp              # Generate new PyRails app
pyrails generate model User    # Generate model boilerplate
pyrails generate controller Users  # Generate controller boilerplate
pyrails db:create              # Create database tables
pyrails db:migrate             # Run migrations
pyrails db:seed                # Load seed data
pyrails db:reset               # Drop, create, migrate, seed
pyrails server                 # Start dev server with auto-reload
pyrails console                # Interactive Python REPL with app context
pyrails admin:create EMAIL     # Create superuser
pyrails routes                 # Show all registered routes
```

### 3. Async-Native

**All database operations are async:**
```python
async def get_users(session: AsyncSession):
    result = await session.execute(select(User))
    return result.scalars().all()
```

**Controllers use async/await:**
```python
@router.get("/users")
async def index(session: AsyncSession = Depends(get_async_session)):
    users = await session.execute(select(User))
    return users.scalars().all()
```

### 4. Batteries Included

**Out of the box:**
- ✅ SQLAlchemy 2.0+ with async support
- ✅ FastAPI Users authentication (JWT, OAuth2)
- ✅ SQLAdmin panel (auto-generated CRUD)
- ✅ Jinja2 templating with hot reload
- ✅ Database seeding system
- ✅ Migration support (Alembic integration)
- ✅ CORS middleware
- ✅ Static file serving
- ✅ Email service (SMTP)
- ✅ Background jobs (optional: Celery integration)

---

## Core Components

### 1. Project Generator (`pyrails new`)

**Command:**
```bash
pyrails new myapp --database postgres
cd myapp
pyrails server  # App running on http://localhost:8000
```

**Generated structure:**
```
myapp/
├── bin/
│   └── pyrails                # Executable wrapper
├── app/
│   ├── models/
│   │   ├── __init__.py
│   │   └── user.py            # Example model
│   ├── controllers/
│   │   ├── __init__.py
│   │   ├── auth_controller.py
│   │   └── users_controller.py
│   ├── services/
│   │   └── __init__.py
│   ├── views/
│   │   ├── layout.html
│   │   ├── index.html
│   │   └── dashboard.html
│   └── assets/
│       ├── css/
│       │   └── application.css
│       └── js/
│           └── application.js
├── config/
│   ├── __init__.py
│   ├── application.py         # Main FastAPI app
│   ├── database.py            # Database config
│   └── settings.py            # Environment settings
├── db/
│   ├── seeds/
│   │   ├── __init__.py
│   │   └── development.py
│   └── migrations/            # Alembic migrations
├── data/                      # Default SQLite location
├── tests/
│   ├── __init__.py
│   └── test_example.py
├── .env.example
├── .env
├── .gitignore
├── .python-version
├── pyproject.toml
├── uv.lock
└── README.md
```

### 2. Model Generator (`pyrails generate model`)

**Command:**
```bash
pyrails generate model Article title:str body:text published_at:datetime user:references
```

**Generates `app/models/article.py`:**
```python
"""Article model - generated by PyRails."""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from .base import Base


class Article(Base):
    """Article model."""
    __tablename__ = "articles"

    id = Column(Integer, primary_key=True)
    title = Column(String(255), nullable=False)
    body = Column(Text)
    published_at = Column(DateTime, nullable=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)

    # Timestamps (auto-managed)
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    # Relationships
    user = relationship("User", back_populates="articles")

    def __repr__(self):
        return f"<Article {self.id}: {self.title}>"
```

**Also updates `app/models/user.py`:**
```python
# Adds to User model:
articles = relationship("Article", back_populates="user")
```

**Field types supported:**
- `str` → `String(255)`
- `text` → `Text`
- `int` → `Integer`
- `float` → `Float`
- `bool` → `Boolean`
- `datetime` → `DateTime`
- `date` → `Date`
- `json` → `JSON`
- `references` → `ForeignKey` (auto-creates relationship)

### 3. Controller Generator (`pyrails generate controller`)

**Command:**
```bash
pyrails generate controller Articles index show create update destroy
```

**Generates `app/controllers/articles_controller.py`:**
```python
"""Articles controller - generated by PyRails."""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.article import Article
from app.models.user import User
from config.database import get_async_session
from app.controllers.auth_controller import current_active_user

router = APIRouter(prefix="/api/articles", tags=["articles"])


@router.get("/")
async def index(
    session: AsyncSession = Depends(get_async_session),
    user: User = Depends(current_active_user)
):
    """List all articles."""
    result = await session.execute(select(Article))
    articles = result.scalars().all()
    return articles


@router.get("/{article_id}")
async def show(
    article_id: int,
    session: AsyncSession = Depends(get_async_session),
    user: User = Depends(current_active_user)
):
    """Get single article."""
    result = await session.execute(
        select(Article).where(Article.id == article_id)
    )
    article = result.scalar_one_or_none()

    if not article:
        raise HTTPException(status_code=404, detail="Article not found")

    return article


@router.post("/")
async def create(
    title: str,
    body: str,
    session: AsyncSession = Depends(get_async_session),
    user: User = Depends(current_active_user)
):
    """Create new article."""
    article = Article(
        title=title,
        body=body,
        user_id=user.id
    )
    session.add(article)
    await session.commit()
    await session.refresh(article)
    return article


@router.put("/{article_id}")
async def update(
    article_id: int,
    title: str | None = None,
    body: str | None = None,
    session: AsyncSession = Depends(get_async_session),
    user: User = Depends(current_active_user)
):
    """Update article."""
    result = await session.execute(
        select(Article).where(Article.id == article_id)
    )
    article = result.scalar_one_or_none()

    if not article:
        raise HTTPException(status_code=404, detail="Article not found")

    if title:
        article.title = title
    if body:
        article.body = body

    await session.commit()
    await session.refresh(article)
    return article


@router.delete("/{article_id}")
async def destroy(
    article_id: int,
    session: AsyncSession = Depends(get_async_session),
    user: User = Depends(current_active_user)
):
    """Delete article."""
    result = await session.execute(
        select(Article).where(Article.id == article_id)
    )
    article = result.scalar_one_or_none()

    if not article:
        raise HTTPException(status_code=404, detail="Article not found")

    await session.delete(article)
    await session.commit()
    return {"message": "Article deleted"}
```

**Auto-registers in `config/application.py`:**
```python
from app.controllers.articles_controller import router as articles_router
app.include_router(articles_router)
```

### 4. Database CLI Commands

**Implementation in PyRails core:**

**`pyrails db:create`** - Create all tables from models
```python
async def db_create():
    """Create database tables."""
    from app.models import Base  # Imports all models
    from config.database import engine

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    print("✅ Database tables created")
```

**`pyrails db:migrate`** - Run Alembic migrations
```python
async def db_migrate():
    """Run database migrations using Alembic."""
    import alembic.config
    alembic_args = [
        '--raiseerr',
        'upgrade', 'head',
    ]
    alembic.config.main(argv=alembic_args)
    print("✅ Migrations completed")
```

**`pyrails db:seed`** - Load seed data
```python
async def db_seed():
    """Load seed data from db/seeds."""
    print("🌱 Seeding database...")

    # Import and run seed file based on environment
    env = os.getenv("PYRAILS_ENV", "development")
    seed_file = Path(f"db/seeds/{env}.py")

    if seed_file.exists():
        import importlib.util
        spec = importlib.util.spec_from_file_location("seeds", seed_file)
        seeds = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(seeds)

        if hasattr(seeds, 'seed'):
            await seeds.seed()
            print("✅ Database seeded successfully")
        else:
            print(f"❌ No seed() function found in {seed_file}")
    else:
        print(f"❌ Seed file not found: {seed_file}")
```

**`pyrails db:reset`** - Drop, create, migrate, seed
```python
async def db_reset():
    """Reset database (drop, create, migrate, seed)."""
    print("⚠️  Resetting database...")

    from app.models import Base
    from config.database import engine

    # Drop all tables
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
    print("  Dropped all tables")

    # Create tables
    await db_create()

    # Run migrations
    await db_migrate()

    # Seed
    await db_seed()

    print("✅ Database reset complete")
```

### 5. Authentication System (FastAPI Users)

**Pre-configured in every new app:**

**`app/models/user.py`:**
```python
from fastapi_users.db import SQLAlchemyBaseUserTable
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import DeclarativeBase
from datetime import datetime


class Base(DeclarativeBase):
    """Base class for all models."""
    pass


class User(SQLAlchemyBaseUserTable[int], Base):
    """User model with FastAPI Users integration."""
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)

    # FastAPI Users fields (inherited):
    # - email: str
    # - hashed_password: str
    # - is_active: bool
    # - is_superuser: bool
    # - is_verified: bool

    # Custom fields
    full_name = Column(String, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
```

**`app/controllers/auth_controller.py`:**
```python
"""Authentication controller - FastAPI Users integration."""
from fastapi import APIRouter, Depends
from fastapi_users import FastAPIUsers
from fastapi_users.authentication import (
    AuthenticationBackend,
    BearerTransport,
    JWTStrategy,
)

from app.models.user import User
from config.database import get_async_session
from config.settings import settings

# JWT transport
bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")

def get_jwt_strategy() -> JWTStrategy:
    return JWTStrategy(secret=settings.SECRET_KEY, lifetime_seconds=3600)

# Auth backend
auth_backend = AuthenticationBackend(
    name="jwt",
    transport=bearer_transport,
    get_strategy=get_jwt_strategy,
)

# FastAPI Users instance
fastapi_users = FastAPIUsers[User, int](
    get_user_manager,
    [auth_backend],
)

# Export dependencies
current_active_user = fastapi_users.current_user(active=True)
current_superuser = fastapi_users.current_user(active=True, superuser=True)

# Router
router = APIRouter(prefix="/auth", tags=["auth"])

# Include auth routes
router.include_router(
    fastapi_users.get_auth_router(auth_backend),
    prefix="/jwt"
)
router.include_router(
    fastapi_users.get_register_router(User, UserRead),
    prefix="/register"
)
```

### 6. Admin Panel (SQLAdmin)

**Auto-configured for all models:**

**`app/controllers/admin_controller.py`:**
```python
"""Admin controller - SQLAdmin auto-generated views."""
from sqladmin import Admin, ModelView
from app.models.user import User
from app.models.article import Article


class UserAdmin(ModelView, model=User):
    """Admin view for User model."""
    column_list = [User.id, User.email, User.full_name, User.is_active, User.created_at]
    column_searchable_list = [User.email, User.full_name]
    column_sortable_list = [User.id, User.email, User.created_at]
    column_default_sort = [(User.created_at, True)]

    # Hide password field
    form_excluded_columns = [User.hashed_password]

    name = "User"
    name_plural = "Users"
    icon = "fa-solid fa-user"


class ArticleAdmin(ModelView, model=Article):
    """Admin view for Article model."""
    column_list = [Article.id, Article.title, Article.user, Article.published_at, Article.created_at]
    column_searchable_list = [Article.title, Article.body]
    column_sortable_list = [Article.id, Article.title, Article.created_at]

    name = "Article"
    name_plural = "Articles"
    icon = "fa-solid fa-newspaper"


# Export all admin views
__all__ = ["UserAdmin", "ArticleAdmin"]
```

**`config/application.py` auto-registers:**
```python
from sqladmin import Admin
from app.controllers import admin_controller

# Setup admin panel
admin = Admin(app, engine, title="MyApp Admin")

# Auto-discover and register all ModelView classes
import inspect
for name, obj in inspect.getmembers(admin_controller):
    if inspect.isclass(obj) and issubclass(obj, ModelView) and obj != ModelView:
        admin.add_view(obj)
```

### 7. Seeding System

**Convention: `db/seeds/{environment}.py` exports `async def seed()`**

**Example `db/seeds/development.py`:**
```python
"""Development seed data."""
import asyncio
from passlib.context import CryptContext
from sqlalchemy import select

from app.models.user import User
from app.models.article import Article
from config.database import async_session_maker

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


async def seed():
    """Seed development database."""
    async with async_session_maker() as session:
        print("\n🌱 Seeding development database...")

        # Create admin user
        result = await session.execute(
            select(User).where(User.email == "admin@example.com")
        )
        admin = result.scalar_one_or_none()

        if not admin:
            admin = User(
                email="admin@example.com",
                hashed_password=pwd_context.hash("admin123"),
                full_name="Admin User",
                is_superuser=True,
                is_active=True,
                is_verified=True,
            )
            session.add(admin)
            await session.flush()
            print("   ✅ Admin user created: admin@example.com / admin123")

        # Create sample articles
        result = await session.execute(select(Article))
        existing_articles = result.scalars().all()

        if not existing_articles:
            articles = [
                Article(
                    title="Welcome to PyRails",
                    body="PyRails brings Rails conventions to FastAPI!",
                    user_id=admin.id
                ),
                Article(
                    title="Getting Started",
                    body="Run 'pyrails server' to start your app",
                    user_id=admin.id
                ),
            ]
            session.add_all(articles)
            print(f"   ✅ Created {len(articles)} sample articles")

        await session.commit()
        print("\n✅ Seeding complete!")


if __name__ == "__main__":
    asyncio.run(seed())
```

**Import existing data example:**
```python
async def seed():
    """Seed from JSON files."""
    import json
    from pathlib import Path

    data_dir = Path("data/import")

    async with async_session_maker() as session:
        # Import users from JSON
        users_file = data_dir / "users.json"
        if users_file.exists():
            with open(users_file) as f:
                users_data = json.load(f)

            for user_data in users_data:
                user = User(**user_data)
                session.add(user)

            print(f"   ✅ Imported {len(users_data)} users")

        await session.commit()
```

### 8. Console (Interactive REPL)

**`pyrails console` opens Python REPL with app context loaded:**

```python
def console():
    """Start interactive console with app context."""
    import code
    from config.application import app
    from config.database import async_session_maker
    from app.models import User, Article
    from sqlalchemy import select
    import asyncio

    banner = """
PyRails Console
---------------
Available: app, async_session_maker, User, Article, select, asyncio

Example - Fetch all users:
  >>> async def get_users():
  ...     async with async_session_maker() as session:
  ...         result = await session.execute(select(User))
  ...         return result.scalars().all()
  >>> users = asyncio.run(get_users())
  >>> print(users)

Example - Create a user:
  >>> async def create_user():
  ...     async with async_session_maker() as session:
  ...         user = User(email="test@example.com", ...)
  ...         session.add(user)
  ...         await session.commit()
  >>> asyncio.run(create_user())
    """

    # Load all models into namespace
    from app import models
    import inspect
    namespace = {"app": app, "async_session_maker": async_session_maker, "asyncio": asyncio}

    for name, obj in inspect.getmembers(models):
        if inspect.isclass(obj):
            namespace[name] = obj

    code.interact(banner=banner, local=namespace)
```

### 9. Server Command

**`pyrails server` starts Uvicorn with auto-reload:**

```python
def server(host: str = "0.0.0.0", port: int = 8000):
    """Start development server."""
    import uvicorn
    from config.settings import settings

    print(f"🚀 Starting PyRails server...")
    print(f"   http://localhost:{port}")
    print(f"   http://localhost:{port}/admin (admin panel)")
    print(f"   http://localhost:{port}/docs (API docs)")
    print("")

    uvicorn.run(
        "config.application:app",
        host=host,
        port=port,
        reload=True,
        reload_dirs=["app", "config"],
    )
```

### 10. Routes Inspector

**`pyrails routes` shows all registered routes:**

```python
def routes():
    """Show all registered routes."""
    from config.application import app
    from rich.console import Console
    from rich.table import Table

    console = Console()
    table = Table(title="PyRails Routes")

    table.add_column("Method", style="cyan")
    table.add_column("Path", style="magenta")
    table.add_column("Name", style="green")
    table.add_column("Handler", style="yellow")

    for route in app.routes:
        if hasattr(route, "methods"):
            for method in route.methods:
                table.add_row(
                    method,
                    route.path,
                    route.name or "",
                    route.endpoint.__name__ if hasattr(route, "endpoint") else ""
                )

    console.print(table)
```

---

## Configuration System

### 1. Settings (Pydantic BaseSettings)

**`config/settings.py`:**
```python
"""Application settings - loaded from environment variables."""
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    """Application settings."""
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False
    )

    # App
    APP_NAME: str = "PyRails App"
    DEBUG: bool = False
    SECRET_KEY: str  # Required

    # Database
    DATABASE_URL: str = "sqlite+aiosqlite:///./data/app.db"

    # CORS
    CORS_ORIGINS: list[str] = ["http://localhost:3000"]

    # Email (optional)
    SMTP_HOST: str | None = None
    SMTP_PORT: int = 587
    SMTP_USER: str | None = None
    SMTP_PASSWORD: str | None = None
    EMAIL_FROM: str | None = None


settings = Settings()
```

### 2. Database Configuration

**`config/database.py`:**
```python
"""Database configuration."""
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import (
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)
from config.settings import settings

# Create async engine
engine = create_async_engine(
    settings.DATABASE_URL,
    echo=settings.DEBUG,
    future=True,
)

# Create session maker
async_session_maker = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,
)


async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
    """FastAPI dependency for async database sessions."""
    async with async_session_maker() as session:
        yield session
```

### 3. Application Configuration

**`config/application.py`:**
```python
"""Main FastAPI application - Rails-style config/application.py."""
import os
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.middleware.cors import CORSMiddleware

# Import all models to ensure they're registered
from app.models import Base

# Import config
from config.database import engine
from config.settings import settings


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application lifespan - startup and shutdown events."""
    # Startup: Create tables if they don't exist
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    print("✅ Database initialized")

    yield

    # Shutdown: Close database connections
    await engine.dispose()
    print("👋 Application shutdown")


# Initialize FastAPI app
app = FastAPI(
    title=settings.APP_NAME,
    version="1.0.0",
    lifespan=lifespan,
    debug=settings.DEBUG,
)

# CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Static files
static_dir = Path(__file__).parent.parent / "app" / "assets"
if static_dir.exists():
    app.mount("/assets", StaticFiles(directory=str(static_dir)), name="assets")

# Templates
templates_dir = Path(__file__).parent.parent / "app" / "views"
templates = Jinja2Templates(directory=str(templates_dir))

# Auto-discover and include all controllers
from app.controllers import (
    auth_controller,
    users_controller,
    # Add new controllers here, or use auto-discovery
)

app.include_router(auth_controller.router)
app.include_router(users_controller.router)

# Setup admin panel
from sqladmin import Admin
from app.controllers import admin_controller
import inspect
from sqladmin import ModelView

admin = Admin(app, engine, title=f"{settings.APP_NAME} Admin")

# Auto-register all ModelView classes from admin_controller
for name, obj in inspect.getmembers(admin_controller):
    if inspect.isclass(obj) and issubclass(obj, ModelView) and obj != ModelView:
        admin.add_view(obj)


@app.get("/")
async def root():
    """Root endpoint."""
    return {
        "message": f"Welcome to {settings.APP_NAME}",
        "docs": "/docs",
        "admin": "/admin"
    }
```

---

## CLI Implementation

### Main Entry Point (`bin/pyrails`)

**Executable script added to PATH:**

```python
#!/usr/bin/env python
"""PyRails CLI - Rails-style commands for Python."""
import sys
import os
from pathlib import Path

# Add current directory to path for project commands
sys.path.insert(0, str(Path.cwd()))


def main():
    """Main CLI entry point."""
    from pyrails.cli import PyRailsCLI

    cli = PyRailsCLI()
    cli.run(sys.argv[1:])


if __name__ == "__main__":
    main()
```

### CLI Framework (`pyrails/cli.py`)

```python
"""PyRails CLI framework."""
import sys
import asyncio
from pathlib import Path
from typing import Optional


class PyRailsCLI:
    """Main CLI handler."""

    def __init__(self):
        self.commands = {
            "new": self.new_app,
            "generate": self.generate,
            "g": self.generate,  # Alias
            "server": self.server,
            "s": self.server,  # Alias
            "console": self.console,
            "c": self.console,  # Alias
            "routes": self.routes,
            "db:create": self.db_create,
            "db:migrate": self.db_migrate,
            "db:seed": self.db_seed,
            "db:reset": self.db_reset,
            "admin:create": self.admin_create,
            "admin:list": self.admin_list,
            "help": self.help,
            "--help": self.help,
            "-h": self.help,
        }

    def run(self, args: list[str]):
        """Execute CLI command."""
        if not args:
            self.help()
            sys.exit(1)

        command = args[0]
        command_args = args[1:]

        if command in self.commands:
            handler = self.commands[command]

            # Run async commands with asyncio
            if asyncio.iscoroutinefunction(handler):
                asyncio.run(handler(*command_args))
            else:
                handler(*command_args)
        else:
            print(f"Unknown command: {command}")
            self.help()
            sys.exit(1)

    def help(self):
        """Print help message."""
        print("""
PyRails - Rails-style CLI for FastAPI

Usage: pyrails COMMAND [options]

🆕 Generation Commands:
  new APP_NAME           Create a new PyRails application
  generate model NAME    Generate a new model
  generate controller NAME   Generate a new controller
  g                      Alias for 'generate'

📦 Database Commands:
  db:create              Create all database tables
  db:migrate             Run database migrations
  db:seed                Load seed data
  db:reset               Drop, create, migrate, and seed (⚠️  destructive!)

👤 Admin Commands:
  admin:create EMAIL [PASSWORD]
                         Create a superuser admin account
  admin:list             List all admin users

🚀 Server Commands:
  server                 Start development server on http://localhost:8000
  s                      Alias for 'server'
  console                Start interactive Python console with app context
  c                      Alias for 'console'
  routes                 Show all registered routes

📝 Examples:
  pyrails new myapp                          Create new app
  pyrails generate model Article title:str  Generate Article model
  pyrails db:reset                           Reset database
  pyrails server                             Start dev server
  pyrails routes                             View all routes
        """)

    def new_app(self, app_name: Optional[str] = None, *args):
        """Generate new PyRails application."""
        if not app_name:
            print("Usage: pyrails new APP_NAME")
            sys.exit(1)

        from pyrails.generators.app_generator import AppGenerator
        generator = AppGenerator(app_name)
        generator.generate()

    def generate(self, resource_type: Optional[str] = None, *args):
        """Generate models, controllers, etc."""
        if not resource_type:
            print("Usage: pyrails generate [model|controller] NAME [fields...]")
            sys.exit(1)

        if resource_type == "model":
            from pyrails.generators.model_generator import ModelGenerator
            generator = ModelGenerator(args)
            asyncio.run(generator.generate())
        elif resource_type == "controller":
            from pyrails.generators.controller_generator import ControllerGenerator
            generator = ControllerGenerator(args)
            generator.generate()
        else:
            print(f"Unknown generator: {resource_type}")
            sys.exit(1)

    # ... other command implementations (db:*, admin:*, etc.)
```

---

## Package Structure

**PyRails as installable package:**

```
pyrails/
├── pyrails/
│   ├── __init__.py
│   ├── cli.py                  # Main CLI
│   ├── generators/
│   │   ├── __init__.py
│   │   ├── app_generator.py    # 'pyrails new'
│   │   ├── model_generator.py  # 'generate model'
│   │   ├── controller_generator.py
│   │   └── templates/          # Jinja2 templates for generation
│   │       ├── app/
│   │       ├── model.py.jinja
│   │       └── controller.py.jinja
│   ├── commands/
│   │   ├── __init__.py
│   │   ├── db.py               # db:* commands
│   │   ├── server.py           # server command
│   │   ├── console.py          # console command
│   │   └── admin.py            # admin:* commands
│   └── templates/              # Full app templates
│       └── new_app/
│           ├── app/
│           ├── config/
│           ├── db/
│           └── ...
├── tests/
├── pyproject.toml
├── README.md
└── LICENSE
```

**`pyproject.toml`:**
```toml
[project]
name = "pyrails"
version = "1.0.0"
description = "Rails-inspired web framework for FastAPI"
authors = [
    {name = "PyRails Contributors", email = "hello@pyrails.dev"}
]
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
classifiers = [
    "Development Status :: 4 - Beta",
    "Framework :: FastAPI",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]

dependencies = [
    "fastapi>=0.115.0",
    "uvicorn>=0.32.0",
    "sqlalchemy>=2.0.0",
    "alembic>=1.13.0",
    "aiosqlite>=0.20.0",  # Default SQLite support
    "asyncpg>=0.29.0",     # PostgreSQL support
    "aiomysql>=0.2.0",     # MySQL support
    "fastapi-users[sqlalchemy]>=13.0.0",
    "sqladmin>=0.19.0",
    "jinja2>=3.1.4",
    "python-dotenv>=1.2.0",
    "pydantic-settings>=2.5.0",
    "passlib[bcrypt]>=1.7.4",
    "python-multipart>=0.0.20",
    "rich>=14.2.0",         # Pretty CLI output
    "typer>=0.12.0",        # CLI framework
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0.0",
    "pytest-asyncio>=0.24.0",
    "httpx>=0.28.0",
    "ruff>=0.7.0",
]

[project.scripts]
pyrails = "pyrails.cli:main"

[project.urls]
Homepage = "https://pyrails.dev"
Documentation = "https://docs.pyrails.dev"
Repository = "https://github.com/pyrails/pyrails"
```

---

## Key Differentiators from Django/Flask

### vs Django

**PyRails:**
- ✅ Async-first (FastAPI/SQLAlchemy async)
- ✅ Lighter weight, faster startup
- ✅ Auto-generated OpenAPI docs
- ✅ Convention over configuration (like Rails)
- ✅ Modern Python (type hints, async/await)

**Django:**
- ✅ More mature ecosystem
- ✅ Built-in ORM with more features
- ✅ Admin panel more customizable
- ❌ Sync by default (async support limited)
- ❌ Heavier, slower for APIs

### vs Flask

**PyRails:**
- ✅ Conventions (Flask is too flexible)
- ✅ Batteries included (auth, admin, ORM)
- ✅ Async-native
- ✅ Type safety with Pydantic
- ✅ CLI tools (generators, migrations)

**Flask:**
- ✅ Extremely flexible
- ✅ Mature ecosystem
- ❌ No conventions (every project different)
- ❌ Sync by default
- ❌ Batteries NOT included

### vs Raw FastAPI

**PyRails:**
- ✅ Structure and conventions
- ✅ Database integration out of box
- ✅ Admin panel included
- ✅ CLI tools for productivity
- ✅ Authentication pre-configured

**FastAPI:**
- ✅ Maximum flexibility
- ✅ Lighter weight
- ❌ No structure (blank canvas)
- ❌ Manual setup for everything
- ❌ No CLI tools

---

## Implementation Roadmap

### Phase 1: Core Framework (MVP)
- ✅ App generator (`pyrails new`)
- ✅ Database configuration
- ✅ Model generator with migrations
- ✅ Controller generator
- ✅ CLI commands (db:*, server, console)
- ✅ Authentication (FastAPI Users)
- ✅ Admin panel (SQLAdmin)

### Phase 2: Enhanced Generators
- ✅ Service generator
- ✅ Migration generator
- ✅ Scaffold generator (model + controller + views)
- ✅ Test generator
- ✅ Mailer generator

### Phase 3: Advanced Features
- ✅ Background jobs (Celery integration)
- ✅ WebSocket support
- ✅ GraphQL support (Strawberry)
- ✅ Asset pipeline (CSS/JS bundling)
- ✅ I18n support
- ✅ Caching layer

### Phase 4: Ecosystem
- ✅ Plugin system
- ✅ Community gems (packages)
- ✅ Deployment tools (Docker, Railway, Fly.io)
- ✅ Documentation site
- ✅ Tutorial videos

---

## Testing Philosophy

**Every generator creates tests:**

**`tests/models/test_article.py`** (auto-generated):
```python
"""Tests for Article model."""
import pytest
from app.models.article import Article
from app.models.user import User


@pytest.mark.asyncio
async def test_create_article(db_session, admin_user):
    """Test creating an article."""
    article = Article(
        title="Test Article",
        body="Test body",
        user_id=admin_user.id
    )
    db_session.add(article)
    await db_session.commit()
    await db_session.refresh(article)

    assert article.id is not None
    assert article.title == "Test Article"
    assert article.user_id == admin_user.id


@pytest.mark.asyncio
async def test_article_user_relationship(db_session, admin_user):
    """Test article-user relationship."""
    article = Article(
        title="Test",
        body="Body",
        user_id=admin_user.id
    )
    db_session.add(article)
    await db_session.commit()

    # Access relationship
    assert article.user.email == admin_user.email
```

**Fixtures in `tests/conftest.py`:**
```python
"""Pytest fixtures for testing."""
import pytest
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

from app.models import Base
from app.models.user import User
from config.settings import settings


@pytest.fixture(scope="session")
def event_loop():
    """Create event loop for async tests."""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()


@pytest.fixture
async def db_engine():
    """Create test database engine."""
    engine = create_async_engine(
        "sqlite+aiosqlite:///:memory:",
        echo=False
    )

    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

    yield engine

    await engine.dispose()


@pytest.fixture
async def db_session(db_engine):
    """Create test database session."""
    async_session_maker = sessionmaker(
        db_engine,
        class_=AsyncSession,
        expire_on_commit=False
    )

    async with async_session_maker() as session:
        yield session


@pytest.fixture
async def admin_user(db_session):
    """Create test admin user."""
    from passlib.context import CryptContext
    pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

    user = User(
        email="admin@test.com",
        hashed_password=pwd_context.hash("testpass"),
        full_name="Test Admin",
        is_superuser=True,
        is_active=True,
        is_verified=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)

    return user
```

---

## Documentation Structure

### 1. Getting Started Guide
```markdown
# Getting Started with PyRails

## Installation
pip install pyrails

## Create Your First App
pyrails new blog
cd blog
pyrails server

## Your First Model
pyrails generate model Article title:str body:text published:bool
pyrails db:migrate

## Your First Controller
pyrails generate controller Articles index show create update destroy

## Run the Server
pyrails server
# Visit http://localhost:8000/docs
```

### 2. Guides
- Models & Database
- Controllers & Routing
- Views & Templates
- Authentication
- Admin Panel
- Testing
- Deployment

### 3. API Reference
- CLI Commands
- Model Field Types
- Controller Helpers
- Template Tags
- Configuration Options

---

## Example Apps (Inspiration)

### 1. Blog (Simple CRUD)
```bash
pyrails new blog
cd blog
pyrails generate model Article title:str body:text author:references
pyrails generate controller Articles
pyrails db:migrate
pyrails db:seed
pyrails server
```

### 2. E-commerce (Complex)
```bash
pyrails new shop
cd shop
pyrails generate model Product name:str price:float inventory:int
pyrails generate model Order user:references status:str total:float
pyrails generate model OrderItem order:references product:references quantity:int
pyrails generate service StripePayment
pyrails db:migrate
```

### 3. SaaS App (Full-stack)
```bash
pyrails new saas
cd saas
pyrails generate model Organization name:str plan:str
pyrails generate model Membership user:references organization:references role:str
pyrails generate service BillingService
pyrails generate service EmailService
pyrails db:migrate
```

---

## Community & Ecosystem

### PyRails Gems (Packages)

**Similar to Ruby gems, Python packages for PyRails:**

- `pyrails-auth-oauth` - OAuth providers (Google, GitHub, etc.)
- `pyrails-payments` - Stripe/PayPal integration
- `pyrails-uploads` - File upload handling (S3, Cloudinary)
- `pyrails-search` - Full-text search (Elasticsearch, Meilisearch)
- `pyrails-cache` - Redis/Memcached integration
- `pyrails-jobs` - Background job processing
- `pyrails-mailer` - Email templates and sending
- `pyrails-websockets` - WebSocket support
- `pyrails-graphql` - GraphQL API generator
- `pyrails-deploy` - Deployment tools (Docker, K8s)

### Installation
```bash
pip install pyrails-auth-oauth
pyrails generate oauth google github
```

---

## Success Metrics

**Goals for PyRails:**

1. **Developer Happiness**
   - Time from idea to deployed app: < 1 hour
   - Lines of boilerplate: < 50 (vs 500+ in raw FastAPI)
   - "It just works" moments: Every feature

2. **Performance**
   - Faster than Django (async advantage)
   - Comparable to raw FastAPI (minimal overhead)
   - Startup time: < 1 second

3. **Adoption**
   - 10,000 GitHub stars in Year 1
   - 100 production apps in Year 1
   - 50+ community packages in Year 1

---

## Key Principles for LLM Implementation

### 1. Start with Templates
All generators use Jinja2 templates in `pyrails/templates/`:
- `model.py.jinja`
- `controller.py.jinja`
- `admin_view.py.jinja`
- `test_model.py.jinja`

### 2. Convention Detection
Automatically infer:
- Table name from class name (`User` → `users`)
- Foreign keys from `references` fields
- Relationship names from model names
- Route prefixes from controller names

### 3. Smart Defaults
- SQLite for development (zero config)
- JWT authentication (secure by default)
- Auto-reload in development
- CORS enabled for localhost:3000

### 4. Progressive Disclosure
- Simple apps require zero config
- Complex apps can override everything
- Configuration lives in `config/`
- Environment variables in `.env`

### 5. Error Messages
- Helpful, actionable error messages
- Suggest fixes (like Rust compiler)
- Point to documentation
- Show examples

### 6. Documentation in Code
- Every generated file has comments
- Examples in docstrings
- Type hints everywhere
- README updated automatically

---

## Final Notes for LLM

**When building PyRails, prioritize:**

1. **Developer Experience** - Make it joyful to use
2. **Conventions** - Strong opinions, loosely held
3. **Performance** - Async all the way down
4. **Simplicity** - Remove unnecessary complexity
5. **Compatibility** - Play well with FastAPI ecosystem

**Inspiration sources:**
- Ruby on Rails (conventions, CLI, generators)
- Django (admin panel, ORM features)
- Laravel (elegant APIs, Artisan CLI)
- FastAPI (type safety, auto docs, performance)

**Non-goals:**
- Don't replace FastAPI (build on top of it)
- Don't rewrite SQLAlchemy (use it well)
- Don't create new standards (use existing)

This is a **framework**, not a library. Opinionated by design. Built for speed and happiness.

---

**End of Specification**

This document contains everything needed to build PyRails from scratch. The framework should feel familiar to Rails developers while embracing Python's async ecosystem and FastAPI's modern design. Convention over configuration. Developer happiness. Ship fast. 🚀
