Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ if [ -d "$PACKAGE_DIR" ]; then

# Build
echo " Building..."
rm -rf dist/
uv build

# Publish via twine
Expand Down
6 changes: 0 additions & 6 deletions example/package-lock.json

This file was deleted.

2 changes: 1 addition & 1 deletion fastapi_startkit/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fastapi-startkit"
version = "0.40.0"
version = "0.40.1"
description = "Fastapi Starter kit components"
authors = [
{name = "Bedram Tamang", email = "tmgbedu@gmail.com"}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
name: broadcasting
description: WebSocket event broadcasting via Reverb — define events, emit to channels, authorize subscribers.
---

# Broadcasting

## Defining an event

Subclass `BroadcastEvent`, implement `broadcast_on()` to declare target channels, and set `payload` with the data to send:

```python
from fastapi_startkit.broadcasting import BroadcastEvent, PrivateChannel

class OrderShipped(BroadcastEvent):
def __init__(self, order_id: int) -> None:
self.payload = {"order_id": order_id, "status": "shipped"}

def broadcast_on(self) -> list:
return [PrivateChannel(f"orders.{self.payload['order_id']}")]
```

- `broadcast_on()` must return a list of `Channel`, `PrivateChannel`, or `PresenceChannel` objects.
- `payload` (dict) is what subscribers receive. Defaults to `{}`.
- The event name on the wire is the class name (`"OrderShipped"`) unless you set `name = "custom.name"` on the class.

## Emitting an event

Call `await .emit()` on an event instance — it dispatches to all channels in `broadcast_on()`:

```python
await OrderShipped(order_id=123).emit()
```

Alternatively use the `broadcast` helper directly:

```python
from fastapi_startkit.broadcasting import broadcast

await broadcast(OrderShipped(order_id=123))
```

## Channel types

| Class | Channel name on wire | Auth required |
|-------|----------------------|---------------|
| `Channel("chat")` | `chat` | No — public, open to all |
| `PrivateChannel("orders.1")` | `private-orders.1` | Yes — checked via `@channel` callback |
| `PresenceChannel("room.1")` | `presence-room.1` | Yes — checked + member tracking |

`PrivateChannel` and `PresenceChannel` automatically prepend `private-` / `presence-` to the name you supply.

## Channel authorization

Private and presence channels require a server-side authorization callback. Register callbacks in `routes/channels.py` using the `@channel` decorator:

```python
# routes/channels.py
from fastapi_startkit.broadcasting import channel

@channel("orders.{order_id}")
async def authorize_orders(user, order_id: int) -> bool:
return user is not None and user.id == order_id

@channel("private-notifications")
async def authorize_notifications(user) -> bool:
return user is not None
```

- The pattern supports `{wildcard}` placeholders. Wildcard values are cast to the declared parameter type (e.g. `order_id: int`).
- `user` is the authenticated user injected from the container's auth service.
- Return `True` to grant access, `False` to deny.
- Private/presence channels with **no registered callback are denied by default** (fail-safe).
- `routes/channels.py` is auto-loaded by `ReverbProvider` on boot.

## Registering the provider

Add `ReverbProvider` to your application's providers list:

```python
from fastapi_startkit.broadcasting import ReverbProvider

app = Application(
providers=[
...,
ReverbProvider,
]
)
```

`ReverbProvider` binds `BroadcastManager` into the container, mounts the Reverb WebSocket endpoint, and registers the `/broadcasting/auth` HTTP route used by Laravel Echo for channel handshakes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
name: console-commands
description: Build artisan CLI commands with Cleo in fastapi-startkit. Use when adding new artisan commands, defining arguments/options, accessing the service container, or registering commands via a provider.
---

# Console Commands

Artisan commands are built on [Cleo](https://github.com/python-poetry/cleo). Each command is a class with a `name`, `description`, optional `arguments`/`options`, and a `handle()` method.

## Defining a command

```python
from cleo.helpers import argument, option
from fastapi_startkit.console import Command

class GreetCommand(Command):
name = "greet"
description = "Greet a user by name."

arguments = [
argument("username", description="The name to greet"),
]
options = [
option("shout", "s", description="Output in uppercase", flag=True),
]

def handle(self):
name = self.argument("username")
msg = f"Hello, {name}!"
if self.option("shout"):
msg = msg.upper()
self.line(msg)
```

Run it:

```bash
uv run artisan greet Alice
uv run artisan greet Alice --shout
```

## Async commands

For commands that call `async` framework code (e.g. ORM queries), wrap with `asyncio.run`:

```python
import asyncio
from fastapi_startkit.console import Command

class SyncUsersCommand(Command):
name = "users:sync"
description = "Synchronise users from the remote API."

def handle(self):
asyncio.run(self.handle_async())

async def handle_async(self):
from app.models import User
users = await User.where("synced", False).get()
self.line(f"Syncing {len(users)} users…")
```

## Accessing the container

`Command` carries a `container` property set by the framework before `handle()` is called. Resolve any bound service:

```python
def handle(self):
config = self.container.make("config")
db = self.container.make("db")
self.line(config.get("app.name"))
```

## Output helpers

| Method | Description |
|--------|-------------|
| `self.line(msg)` | Print a line |
| `self.info(msg)` | Print in green |
| `self.comment(msg)` | Print in yellow |
| `self.error(msg)` | Print in red |
| `self.question(msg)` | Print in cyan |
| `self.line_error(msg)` | Print to stderr |
| `self.ask(question)` | Prompt for text input |
| `self.confirm(question)` | Prompt for yes/no |

## Registering commands in a provider

Expose commands from a service provider's `boot()` method:

```python
from fastapi_startkit.providers import Provider
from app.commands import GreetCommand, SyncUsersCommand

class AppServiceProvider(Provider):
def boot(self) -> None:
self.commands([GreetCommand, SyncUsersCommand])
```

The provider must be registered in `bootstrap/application.py` for the commands to appear in `uv run artisan list`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
name: orm-migrations
description: Create and run database schema migrations using the fastapi-startkit ORM. Use when creating tables, adding/dropping columns, or managing migration lifecycle with artisan commands.
---

# ORM Migrations

Migrations describe schema changes as versioned Python files. The `Blueprint` builder maps to SQL DDL for SQLite, MySQL, and PostgreSQL.

## Creating a migration

```bash
uv run artisan make:migration create_users_table
```

This generates a file in `databases/migrations/` with `up()` and `down()` methods.

## Migration class structure

```python
from fastapi_startkit.masoniteorm.migrations import Migration

class CreateUsersTable(Migration):
async def up(self):
async with await self.schema.create("users") as table:
table.increments("id")
table.string("name")
table.string("email").unique()
table.string("password")
table.enum("role", ["admin", "user"]).default("user")
table.boolean("active").default(True)
table.timestamps()

async def down(self):
await self.schema.drop("users")
```

## Common Blueprint column types

| Method | SQL type |
|--------|----------|
| `increments("id")` | auto-increment primary key |
| `string("col", length=255)` | VARCHAR |
| `text("col")` | TEXT |
| `integer("col")` | INT |
| `big_integer("col")` | BIGINT |
| `boolean("col")` | BOOLEAN / TINYINT(1) |
| `decimal("col", precision, scale)` | DECIMAL |
| `float_type("col")` | FLOAT |
| `date("col")` | DATE |
| `datetime("col")` | DATETIME |
| `timestamp("col")` | TIMESTAMP |
| `timestamps()` | `created_at` + `updated_at` |
| `soft_deletes()` | `deleted_at` nullable |
| `enum("col", ["a", "b"])` | ENUM |
| `json("col")` | JSON / TEXT |
| `foreign("col")` | foreign key column |
| `uuid("col")` | UUID / CHAR(36) |

## Column modifiers

Chain these after any column method:

```python
table.string("email").unique()
table.string("bio").nullable()
table.string("status").default("active")
table.string("code").unsigned()
table.integer("views").after("title") # MySQL only
```

## Altering an existing table

```python
async def up(self):
async with await self.schema.table("users") as table:
table.add_column("phone", "string", nullable=True)
table.drop_column("legacy_field")
table.rename_column("old_name", "new_name")
```

## Running migrations

```bash
# Run all pending migrations
uv run artisan db:migrate

# Check migration status
uv run artisan migrate:status

# Roll back the last batch
uv run artisan migrate:rollback

# Drop all tables and re-run from scratch
uv run artisan migrate:fresh
```

## Migration directory

By default migrations are read from `databases/migrations/`. Configure a custom path in `config/database.py`:

```python
@dataclass
class DatabaseConfig:
migrations: dict = field(default_factory=lambda: {
"directory": "databases/migrations"
})
```
Loading