Metadata-Version: 2.3
Name: fastique
Version: 0.1.0
Summary: FastAPI wrapper to create Django like experience which uses Piccolo ORM under the hood.
Author: Narayan Gautam
Author-email: Narayan Gautam <gnarayyan@gmail.com>
Requires-Dist: fastapi[standard]>=0.136.3
Requires-Dist: itsdangerous>=2.2.0
Requires-Dist: piccolo[all]>=1.34.0
Requires-Dist: pwdlib[argon2]>=0.3.0
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# ⚡ Fastique

**Django-like developer experience, powered by FastAPI + Piccolo ORM.**

Fastique gives you the familiar Django workflow — `startproject`, `startapp`, `migrate`, `createsuperuser`, class-based views, an auto-generated admin panel — all running on the blazing-fast async FastAPI + Piccolo ORM stack.

---

## Feature Overview

| Django feature | Fastique equivalent |
|---|---|
| `django-admin startproject` | `fq startproject` |
| `python manage.py startapp` | `fq startapp` |
| `python manage.py runserver` | `fq runserver` |
| `python manage.py makemigrations` | `fq makemigrations` |
| `python manage.py migrate` | `fq migrate` |
| `python manage.py createsuperuser` | `fq createsuperuser` |
| `python manage.py shell` | `fq shell` |
| `python manage.py test` | `fq test` |
| `models.Model` | `fastique.Model` (Piccolo Table) |
| `admin.site.register` | `@register(MyModel)` |
| `ModelAdmin` | `fastique.ModelAdmin` |
| `ListView`, `DetailView`, etc. | Same names, async |
| `settings.py` | Same — loaded via `FASTIQUE_SETTINGS_MODULE` |
| `urls.py` / `include()` | `Router` + `include()` |
| `@login_required` | Same |
| JWT tokens | Built-in via `/auth/token` |
| Session auth | Built-in via `/auth/login` |

---

## Installation

```bash
pip install fastique
```

---

## Quickstart

### 1. Create a project

```bash
fq startproject myblog
cd myblog
```

Generated structure:

```
myblog/
├── manage.py              ← entry point (like Django's)
├── myblog/
│   ├── __init__.py
│   ├── settings.py        ← all settings in one place
│   ├── asgi.py            ← ASGI app (points uvicorn here)
│   └── urls.py            ← project-level router hints
├── templates/
│   └── base.html
├── static/
├── media/
├── piccolo_conf.py        ← Piccolo migration config
├── .env                   ← environment variables
├── requirements.txt
└── README.md
```

### 2. Create an app

```bash
fq startapp blog
```

Generated app structure:

```
blog/
├── __init__.py
├── models.py       ← your Piccolo Table models
├── routes.py       ← FastAPI router (like views.py + urls.py)
├── admin.py        ← register models with the admin
├── schemas.py      ← Pydantic request/response schemas
├── services.py     ← business logic layer
├── tests.py        ← pytest tests
├── piccolo_app.py  ← Piccolo migration config for this app
└── migrations/     ← auto-generated migration files
```

### 3. Register the app

```python
# myblog/settings.py
INSTALLED_APPS = [
    "blog",
]
```

### 4. Define models

```python
# blog/models.py
from fastique import Model, CharField, TextField, BooleanField, DateTimeField, ForeignKey
from fastique.auth.models import User
from datetime import datetime, timezone

class Article(Model):
    title = CharField(max_length=200)
    slug = CharField(max_length=220, unique=True)
    body = TextField()
    author = ForeignKey(references=User)
    is_published = BooleanField(default=False)
    created_at = DateTimeField(default=datetime.now(tz=timezone.utc))

    class Meta:
        tablename = "blog_articles"
        ordering = ["-created_at"]

    def __str__(self):
        return self.title
```

### 5. Register with admin

```python
# blog/admin.py
from fastique import register, ModelAdmin
from blog.models import Article

@register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = ["id", "title", "author", "is_published", "created_at"]
    list_filter = ["is_published"]
    search_fields = ["title", "body"]
    ordering = ["-created_at"]
    readonly_fields = ["created_at"]
```

### 6. Write routes

```python
# blog/routes.py
from fastique import Router, login_required
from starlette.requests import Request
from starlette.responses import JSONResponse
from blog.models import Article
from blog.schemas import ArticleSchema, ArticleCreateSchema

router = Router(prefix="/blog", tags=["Blog"])

@router.get("/")
async def list_articles(request: Request, page: int = 1) -> JSONResponse:
    articles = await Article.select().output(load_json=True)
    return JSONResponse({"articles": articles})

@router.get("/{slug}")
async def get_article(slug: str) -> JSONResponse:
    rows = await Article.select().where(Article.slug == slug).output(load_json=True)
    if not rows:
        return JSONResponse({"detail": "Not found."}, status_code=404)
    return JSONResponse(rows[0])

@router.post("/", status_code=201)
@login_required
async def create_article(request: Request, data: ArticleCreateSchema) -> JSONResponse:
    article = Article(**data.model_dump())
    await article.save().run()
    return JSONResponse({"id": article.id, "detail": "Created."})
```

### 7. Run migrations

```bash
fq makemigrations blog   # generate migration file
fq migrate blog          # apply it
```

### 8. Create a superuser

```bash
fq createsuperuser
# Username: admin
# Email: admin@example.com
# Password: ••••••••
```

### 9. Start the server

```bash
fq runserver
```

```
╭──────────────────────────────────────────╮
│  Starting Fastique development server    │
│  http://127.0.0.1:8000                   │
│  Admin: http://127.0.0.1:8000/admin/     │
│  API Docs: http://127.0.0.1:8000/docs    │
│  Quit with CTRL+C                        │
╰──────────────────────────────────────────╯
```

---

## Models

```python
from fastique import (
    Model,
    CharField, TextField, IntegerField, FloatField, BooleanField,
    DateField, DateTimeField, ForeignKey, ManyToManyField,
    EmailField, URLField, UUIDField, JSONField,
)
```

### QuerySet API

```python
# ORM-style (async)
articles = await Article.objects.all()
article  = await Article.objects.get(id=1)          # raises Http404 if missing
articles = await Article.objects.filter(is_published=True)
article, created = await Article.objects.get_or_create(slug="hello", defaults={"title": "Hello"})
article  = await Article.objects.create(title="New Post", slug="new-post")

# Piccolo native (more expressive)
articles = await Article.select().where(Article.is_published == True).order_by(Article.created_at, ascending=False).output(load_json=True)
```

---

## Class-Based Views

```python
from fastique import ListView, DetailView, CreateView, UpdateView, DeleteView

class ArticleList(ListView):
    model = Article
    template_name = "blog/list.html"
    paginate_by = 10
    ordering = "-created_at"

class ArticleDetail(DetailView):
    model = Article
    template_name = "blog/detail.html"
    lookup_field = "slug"

# Register with router:
router.get("/")(ArticleList.as_route())
router.get("/{slug}")(ArticleDetail.as_route())
```

---

## Authentication

### Session-based

```bash
POST /auth/login      {"username": "...", "password": "..."}
POST /auth/logout
GET  /auth/me
POST /auth/register   {"username": "...", "email": "...", "password": "..."}
```

### JWT / Token

```bash
POST /auth/token      {"username": "...", "password": "..."}
# Returns: {"access_token": "eyJ...", "token_type": "bearer"}

# Use in headers:
Authorization: Bearer eyJ...
```

### Decorators

```python
from fastique import login_required, staff_required, superuser_required, permission_required

@router.get("/dashboard")
@login_required
async def dashboard(request): ...

@router.get("/admin-only")
@staff_required
async def admin_view(request): ...

@router.delete("/nuke")
@superuser_required
async def dangerous(request): ...

@router.post("/publish")
@permission_required("blog.publish_article")
async def publish(request): ...
```

---

## Settings Reference

```python
# settings.py

SECRET_KEY = "your-secret-key"
DEBUG = True
ALLOWED_HOSTS = ["*"]

INSTALLED_APPS = ["blog", "shop"]

DATABASES = {
    "default": {
        "ENGINE": "sqlite",          # or "postgresql"
        "NAME": "db.sqlite3",
    }
}

# PostgreSQL:
# DATABASES = {
#     "default": {
#         "ENGINE": "postgresql",
#         "NAME": "mydb",
#         "USER": "postgres",
#         "PASSWORD": "secret",
#         "HOST": "localhost",
#         "PORT": 5432,
#     }
# }

TEMPLATES = [{"BACKEND": "jinja2", "DIRS": ["templates"], "APP_DIRS": True}]

STATIC_URL = "/static/"
STATIC_ROOT = "staticfiles"
MEDIA_URL = "/media/"
MEDIA_ROOT = "media"

CORS_ALLOW_ALL_ORIGINS = True    # dev only
CORS_ALLOWED_ORIGINS = ["https://myfrontend.com"]

ADMIN_URL = "admin/"
ADMIN_SITE_HEADER = "My Site Admin"

ACCESS_TOKEN_EXPIRE_MINUTES = 1440  # 24 hours
```

---

## CLI Reference

```bash
fq startproject <name>          Create a new project
fq startapp <name>              Create a new app
fq runserver [host:port]        Run dev server (default: 127.0.0.1:8000)
fq makemigrations [app]         Generate migration files
fq migrate [app]                Apply pending migrations
fq createsuperuser              Create a superuser interactively
fq shell                        Interactive Python shell with context
fq dbshell                      Open the database CLI
fq collectstatic                Copy static files to STATIC_ROOT
fq check                        Run system checks
fq showmigrations [app]         Show migration status
fq routes                       List all registered routes
fq test [args...]               Run pytest
fq version                      Show version info
```

---

## Project Layout (full example)

```
myproject/
├── manage.py
├── myproject/
│   ├── settings.py
│   ├── asgi.py
│   └── urls.py
├── blog/
│   ├── models.py
│   ├── routes.py
│   ├── admin.py
│   ├── schemas.py
│   ├── services.py
│   ├── tests.py
│   ├── piccolo_app.py
│   ├── migrations/
│   └── templates/
│       └── blog/
│           ├── list.html
│           └── detail.html
├── templates/
│   └── base.html
├── static/
├── media/
├── piccolo_conf.py
├── .env
└── requirements.txt
```

---

## License

MIT