diff --git a/bin/release.sh b/bin/release.sh index c963caa8..4618e188 100755 --- a/bin/release.sh +++ b/bin/release.sh @@ -22,6 +22,7 @@ if [ -d "$PACKAGE_DIR" ]; then # Build echo " Building..." + rm -rf dist/ uv build # Publish via twine diff --git a/example/package-lock.json b/example/package-lock.json deleted file mode 100644 index 6e4aa92b..00000000 --- a/example/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "example", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/fastapi_startkit/pyproject.toml b/fastapi_startkit/pyproject.toml index 4cc4d042..2f70ef48 100644 --- a/fastapi_startkit/pyproject.toml +++ b/fastapi_startkit/pyproject.toml @@ -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"} diff --git a/fastapi_startkit/src/fastapi_startkit/skills/stubs/.ai/fastapi-startkit/skill/broadcasting/SKILL.md b/fastapi_startkit/src/fastapi_startkit/skills/stubs/.ai/fastapi-startkit/skill/broadcasting/SKILL.md new file mode 100644 index 00000000..69a86485 --- /dev/null +++ b/fastapi_startkit/src/fastapi_startkit/skills/stubs/.ai/fastapi-startkit/skill/broadcasting/SKILL.md @@ -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. diff --git a/fastapi_startkit/src/fastapi_startkit/skills/stubs/.ai/fastapi-startkit/skill/console-commands/SKILL.md b/fastapi_startkit/src/fastapi_startkit/skills/stubs/.ai/fastapi-startkit/skill/console-commands/SKILL.md new file mode 100644 index 00000000..3f714ee9 --- /dev/null +++ b/fastapi_startkit/src/fastapi_startkit/skills/stubs/.ai/fastapi-startkit/skill/console-commands/SKILL.md @@ -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`. diff --git a/fastapi_startkit/src/fastapi_startkit/skills/stubs/.ai/fastapi-startkit/skill/orm-migrations/SKILL.md b/fastapi_startkit/src/fastapi_startkit/skills/stubs/.ai/fastapi-startkit/skill/orm-migrations/SKILL.md new file mode 100644 index 00000000..9c8d2c38 --- /dev/null +++ b/fastapi_startkit/src/fastapi_startkit/skills/stubs/.ai/fastapi-startkit/skill/orm-migrations/SKILL.md @@ -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" + }) +```