Metadata-Version: 2.4
Name: fasorm
Version: 0.1.11
Summary: Laravel Eloquent-style ORM for FastAPI — expressive models, migrations, and CLI.
License: MIT
Project-URL: Homepage, https://fasorm.sandeshsatyal.com.np
Project-URL: Documentation, https://fasorm.sandeshsatyal.com.np
Project-URL: Repository, https://github.com/sandysh/fasorm
Project-URL: Issues, https://github.com/sandysh/fasorm/issues
Keywords: fastapi,orm,sqlalchemy,migrations,eloquent,active-record
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: sqlalchemy[asyncio]>=2.0
Requires-Dist: alembic>=1.12
Requires-Dist: click>=8.0
Requires-Dist: python-dotenv>=1.0
Requires-Dist: rich>=13.0
Requires-Dist: jinja2>=3.0
Provides-Extra: pg
Requires-Dist: asyncpg>=0.29; extra == "pg"
Requires-Dist: psycopg2-binary>=2.9; extra == "pg"
Provides-Extra: sqlite
Requires-Dist: aiosqlite>=0.20; extra == "sqlite"
Provides-Extra: all
Requires-Dist: asyncpg>=0.29; extra == "all"
Requires-Dist: psycopg2-binary>=2.9; extra == "all"
Requires-Dist: aiosqlite>=0.20; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: aiosqlite>=0.20; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"

# FasORM

**Laravel Eloquent-style ORM for FastAPI** — expressive models, clean migrations, and an artisan-style CLI, powered by SQLAlchemy 2.0 & Alembic.

📖 **Documentation**: [https://fasorm.sandeshsatyal.com.np](https://fasorm.sandeshsatyal.com.np)

```
pip install fasorm
```

---

## Quick Start

### 1. Initialise your project

```bash
fasorm init
```

Creates:
```
app/
    models/
    migrations/
    seeders/
.env
fasorm.config.py
```

### 2. Create a model

```bash
fasorm make:model User -m
```

Edit `app/models/user.py`:

```python
from fasorm import Model, String, Boolean

class User(Model):
    name = String()
    email = String().unique()
    password = String()
    is_active = Boolean().default(True)
```

### 3. Edit the migration

`app/migrations/2026_07_25_143000_create_users_table.py`:

```python
from fasorm.schema import Blueprint

def up(table: Blueprint):
    table.id()
    table.string("name")
    table.string("email").unique()
    table.string("password")
    table.boolean("is_active").default(True)
    table.timestamps()

def down(table: Blueprint):
    table.drop()
```

### 4. Run migrations

```bash
fasorm migrate
```

```
  FasORM  ·  Running migrations…

  3 migration(s) pending…

  ✅  2026_07_25_143000_create_users_table

  ✓  Migration successful. 1 migration(s) applied.
```

---

## Models

### Define a model

```python
from fasorm import Model, String, Boolean, Timestamp, HasMany

class User(Model):
    name = String()
    email = String().unique()
    is_active = Boolean().default(True)
    posts = HasMany("Post")
```

Features:
- Auto-generated `id` (BigInteger, autoincrement)
- Auto-generated `created_at` and `updated_at` timestamps
- Auto-generated `__tablename__` from class name (`User` → `users`, `BlogPost` → `blog_posts`)

### CRUD Operations

```python
# Create
user = await User.create(name="Sandy", email="sandy@example.com")

# Find by ID
user = await User.find(1)
user = await User.find_or_fail(1)  # raises ModelNotFoundError

# Query
users = await User.where(is_active=True).get()
user = await User.where(email="sandy@example.com").first()
users = await User.where("age", ">=", 18).order_by("name").get()

# All
users = await User.all()

# Paginate
page = await User.paginate(per_page=20, page=1)

# Update
await user.update(name="New Name")

# Delete
await user.delete()

# First or create
user = await User.first_or_create(
    {"email": "sandy@example.com"},
    defaults={"name": "Sandy"}
)

# Update or create
user = await User.update_or_create(
    {"email": "sandy@example.com"},
    defaults={"name": "Updated Sandy"}
)
```

### Relationships

```python
from fasorm import Model, String, HasMany, BelongsTo

class User(Model):
    name = String()
    posts = HasMany("Post")

class Post(Model):
    title = String()
    user = BelongsTo("User")

# Eager loading (loaded relations are automatically included in to_dict / to_json)
users = await User.with_("posts").get()
total = await User.with_("posts").where(is_active=True).count()

# Relationship count aggregation (adds `posts_count` attribute)
users = await User.with_count("posts").get()
print(users[0].posts_count)  # → 3
users_dict = await User.with_count("posts").to_dict()  # → [{"id": 1, "name": "Sandy", "posts_count": 3}]

# Aggregates & Model classmethods
count = await User.count()
exists = await User.where(email="sandy@example.com").exists()
total_age = await User.sum("age")
avg_age = await User.avg("age")
```

### Serialisation

```python
# Model instance serialisation (automatically includes eager-loaded relations)
user.to_dict()   # → {"id": 1, "name": "Sandy", "posts": [...]}
user.to_json()   # → '{"id": 1, "name": "Sandy", "posts": [...]}'

# Direct query builder serialisation
users_dict = await User.with_("posts").to_dict()
users_json = await User.with_("posts").to_json()

# Pagination result serialisation
page = await User.paginate(per_page=20, page=1)
page.to_dict()   # → {"data": [...], "meta": {"total": 100, ...}}
```

---

## Field Types

| Field | SQL Type | Notes |
|-------|----------|-------|
| `String(length=255)` | `VARCHAR(n)` | |
| `Char(length=255)` | `CHAR(n)` | Fixed-length string |
| `Text()` | `TEXT` | |
| `TinyText()` | `TINYTEXT` / `TEXT` | |
| `MediumText()` | `MEDIUMTEXT` / `TEXT` | |
| `LongText()` | `LONGTEXT` / `TEXT` | |
| `Integer()` | `INTEGER` | |
| `BigInteger()` | `BIGINT` | |
| `SmallInteger()` | `SMALLINT` | |
| `TinyInteger()` | `TINYINT` / `SMALLINT` | |
| `MediumInteger()` | `MEDIUMINT` / `INTEGER` | |
| `UnsignedInteger()` | `UNSIGNED INT` | |
| `UnsignedBigInteger()` | `UNSIGNED BIGINT` | |
| `UnsignedSmallInteger()` | `UNSIGNED SMALLINT` | |
| `UnsignedTinyInteger()` | `UNSIGNED TINYINT` | |
| `Float(precision)` | `FLOAT` | |
| `Decimal(precision, scale)` | `NUMERIC(p,s)` | |
| `Boolean()` | `BOOLEAN` | |
| `Date()` | `DATE` | |
| `Time()` | `TIME` | |
| `TimeTz()` | `TIME WITH TZ` | Timezone aware |
| `DateTime()` | `DATETIME` | |
| `DateTimeTz()` | `DATETIME WITH TZ` | Timezone aware |
| `Timestamp()` | `TIMESTAMP WITH TZ` | Timezone aware |
| `TimestampTz()` | `TIMESTAMP WITH TZ` | Timezone aware |
| `Year()` | `YEAR` / `INTEGER` | |
| `Json()` | `JSON` / `JSONB` | |
| `Jsonb()` | `JSONB` / `JSON` | Native on PG |
| `Uuid()` | `UUID` / `CHAR(36)` | Native on PG |
| `Ulid()` | `CHAR(26)` | 26-char sortable ID |
| `IpAddress()` | `VARCHAR(45)` | IP Address |
| `MacAddress()` | `VARCHAR(17)` | MAC Address |
| `Enum(values)` | `ENUM` | |
| `Set(values)` | `SET` / `ENUM` | Set of values |
| `Vector(dimension)` | `VECTOR(n)` / `TEXT` | pgvector on PG |
| `Array(item_type)` | `ARRAY` / `JSON` | Native on PG |
| `Binary(length)` | `LARGEBINARY` | |

All fields support chaining: `.nullable()`, `.unique()`, `.default(value)`, `.index()`, `.comment(text)`

---

## CLI Commands

| Command | Description |
|---------|-------------|
| `fasorm init` | Scaffold project structure |
| `fasorm make:model Name [-m]` | Create a model (optionally with migration) |
| `fasorm make:migration name` | Create a migration file |
| `fasorm make:seeder Name` | Create a seeder file |
| `fasorm migrate` | Run pending migrations |
| `fasorm rollback [--steps N]` | Rollback last N batches |
| `fasorm refresh` | Rollback all + re-migrate |
| `fasorm fresh [--seed]` | Drop all + migrate + seed |
| `fasorm seed [--class Name]` | Run seeders |
| `fasorm status` | Show migration status |

---

## FastAPI Integration

```python
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from fasorm import get_session, init_db
from fasorm.config import load_config

app = FastAPI()

@app.on_event("startup")
async def startup():
    config = load_config()
    init_db(config)

@app.get("/users")
async def list_users():
    users = await User.all()
    return [u.to_dict() for u in users]

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await User.find_or_fail(user_id)
    return user.to_dict()
```

---

## Architecture

```
                FastAPI
                   │
                FasORM
                   │
          ─────────┼─────────
          │                 │
      SQLAlchemy         Alembic
          │                 │
      PostgreSQL          SQLite
      MySQL               MariaDB
```

FasORM is an **opinionated layer** on top of SQLAlchemy 2.0 and Alembic — not a replacement.

---

## License

MIT
