Metadata-Version: 2.4
Name: qstack
Version: 0.2.1
Summary: Batteries-included FastAPI building blocks: auth, database, events, permissions, email, pagination, Redis, rate limiting, health checks.
Project-URL: Homepage, https://github.com/yourname/qstack
Project-URL: Documentation, https://github.com/yourname/qstack/blob/main/DOCUMENTATION.md
Project-URL: CLI, https://github.com/yourname/qstack/blob/main/CLI.md
Project-URL: Issues, https://github.com/yourname/qstack/issues
Author: Jonah
License-Expression: MIT
License-File: LICENSE
Keywords: async,auth,events,fastapi,jwt,permissions,qstack,sqlalchemy
Classifier: Framework :: FastAPI
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: aiosmtplib>=2.0
Requires-Dist: fastapi>=0.100.0
Requires-Dist: jinja2>=3.1
Requires-Dist: passlib[bcrypt]>=1.7
Requires-Dist: pydantic-settings>=2.0
Requires-Dist: python-jose[cryptography]>=3.3
Requires-Dist: sqlalchemy[asyncio]>=2.0
Provides-Extra: all
Requires-Dist: asyncpg; extra == 'all'
Requires-Dist: psycopg2-binary; extra == 'all'
Requires-Dist: redis>=5.0; extra == 'all'
Provides-Extra: dev
Requires-Dist: aiosqlite; extra == 'dev'
Requires-Dist: bcrypt<4.2.1; extra == 'dev'
Requires-Dist: build; extra == 'dev'
Requires-Dist: coverage; extra == 'dev'
Requires-Dist: fakeredis[json]; extra == 'dev'
Requires-Dist: httpx; extra == 'dev'
Requires-Dist: passlib[bcrypt]>=1.7; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Provides-Extra: postgres
Requires-Dist: asyncpg; extra == 'postgres'
Requires-Dist: psycopg2-binary; extra == 'postgres'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Description-Content-Type: text/markdown

# qstack

Batteries-included building blocks for FastAPI: auth with account activation, async SQLAlchemy, role-based permissions, an in-process event bus, pagination, Redis utilities, email, health checks.

## What is this?

Most FastAPI projects rebuild the same foundation from scratch: JWT auth, database layer, exception handling, pagination, Redis helpers, middleware. `qstack` ships all of it as composable primitives you drop into any async project. It's not a framework — no magic, no required directory layout, no hidden global state. Every component is a factory you wire up yourself.

## Features

| Feature | Description |
|---------|-------------|
| App factory | `create_app()` pre-wires exception handlers, CORS, request logging |
| Settings | `QSettings` via pydantic-settings with `.env` and SMTP mixin |
| Async SQLAlchemy | `Base` with `id`/`created_at`/`updated_at`, engine/session factories |
| Repository | `BaseRepository[T]` CRUD + filtering + pagination + optional event emission |
| Service layer | `BaseService[T]` with ownership checks |
| Auth | JWT, bcrypt, brute-force protection, `/auth/register`, `/auth/login`, `/auth/me` |
| Account activation | Redis-backed UUID tokens, auto-email, `/auth/activate/{token}`, `/auth/resend-activation` |
| Events | In-process `EventBus` with sync+async listeners, auto-emitted from repo/service |
| Permissions | `Role`/`Permission` models, `PolicyEnforcer`, `require_permission` / `require_role` deps |
| Exceptions | `QException` hierarchy auto-converts to JSON |
| Pagination | `PaginationParams` dependency + generic `PaginatedResponse[T]` |
| Redis | Client factory, JSON cache, rate limiter, token blacklist |
| Email | `EmailService` (SMTP + Jinja2 templates), `send`, `send_template`, `send_bulk` |
| Health | `/health` endpoint verifying DB connectivity |
| Logging middleware | Method, path, status, duration per request |

## Installation

```bash
pip install qstack                 # Core only
pip install qstack[postgres]       # + asyncpg, psycopg2-binary
pip install qstack[redis]          # + redis
pip install qstack[all]            # All extras
pip install qstack[all,dev]        # + pytest, ruff, fakeredis, aiosqlite
```

Python 3.10+.

## Quick Start

```python
# main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI

from qstack.app import create_app
from qstack.auth.models import UserMixin
from qstack.auth.router import create_auth_router
from qstack.config import QSettings
from qstack.database.base import Base
from qstack.database.dependencies import get_db as _get_db
from qstack.database.session import create_engine, create_session_factory
from qstack.health.router import create_health_router


class User(UserMixin, Base):
    __tablename__ = "users"


settings = QSettings(DATABASE_URL="sqlite+aiosqlite:///./app.db")
engine = create_engine(settings.DATABASE_URL)
SessionLocal = create_session_factory(engine)


async def get_db():
    async for s in _get_db(SessionLocal):
        yield s


def user_factory(email, username, hashed_password):
    return User(email=email, username=username, hashed_password=hashed_password)


@asynccontextmanager
async def lifespan(app: FastAPI):
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield
    await engine.dispose()


app = create_app(settings, title="Demo", lifespan=lifespan)
app.include_router(create_auth_router(
    user_model=User,
    get_db=get_db,
    jwt_secret=settings.JWT_SECRET,
    jwt_algorithm=settings.JWT_ALGORITHM,
    access_token_expire_minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES,
    user_factory=user_factory,
))
app.include_router(create_health_router(engine))
```

```bash
pip install "qstack[all]" uvicorn aiosqlite
uvicorn main:app --reload
```

Available: `POST /auth/register`, `POST /auth/login`, `GET /auth/me`, `GET /health`.

To enable account activation add `redis_client=redis, email_service=email, require_activation=True` to `create_auth_router`. See [DOCUMENTATION.md — Account Activation](./DOCUMENTATION.md#account-activation).

## Project Structure

```
my_api/
├── app/
│   ├── main.py           # create_app, mount routers
│   ├── config.py         # Settings(QSettings)
│   ├── database.py       # engine, SessionLocal, get_db
│   ├── models.py         # SQLAlchemy models (Base subclasses)
│   ├── repositories.py   # BaseRepository subclasses
│   ├── services.py       # BaseService subclasses
│   ├── routers/          # domain routes
│   └── policies/         # PolicyEnforcer classes
├── alembic/
├── requirements.txt
└── .env
```

See [`DOCUMENTATION.md`](./DOCUMENTATION.md#integration-guide).

## Documentation

- [`DOCUMENTATION.md`](./DOCUMENTATION.md) — full library API reference, examples, integration guide.
- [`CLI.md`](./CLI.md) — `qstack` CLI reference (project scaffolding, code generation, Alembic wrappers, dev/test/doctor).

## CLI (qstack)

```bash
pip install qstack-cli

qstack new my-api              # interactive project scaffold
qstack generate crud Task      # model + schema + repo + service + router
qstack db migrate "init"       # alembic autogenerate wrapper
qstack dev                     # docker compose up + uvicorn --reload
qstack doctor                  # health check
```

Full reference: [`CLI.md`](./CLI.md).

## Configuration

`QSettings` loads from env vars / `.env`:

| Variable | Default | Notes |
|----------|---------|-------|
| `DATABASE_URL` | `postgresql+asyncpg://...` | async DB URL |
| `DATABASE_SYNC_URL` | `postgresql+psycopg2://...` | for Alembic |
| `JWT_SECRET` | `change-me` | sign tokens |
| `JWT_ALGORITHM` | `HS256` | |
| `JWT_ACCESS_TOKEN_EXPIRE_MINUTES` | `30` | |
| `JWT_REFRESH_TOKEN_EXPIRE_MINUTES` | `10080` | 7 days |
| `CORS_ORIGINS` | `["*"]` | |
| `REDIS_URL` | `redis://localhost:6379/0` | |
| `SMTP_HOST` | `localhost` | |
| `SMTP_PORT` | `587` | |
| `SMTP_USERNAME` | `""` | |
| `SMTP_PASSWORD` | `""` | |
| `SMTP_FROM_EMAIL` | `noreply@example.com` | |
| `SMTP_USE_TLS` | `True` | |

Subclass to add project-specific fields:

```python
from qstack.config import QSettings

class Settings(QSettings):
    STRIPE_API_KEY: str = ""

settings = Settings()
```

## Contributing

```bash
git clone https://github.com/yourname/qstack.git
cd qstack
pip install -e ".[all,dev]"
pytest
ruff check qstack/
```

## License

MIT
