Skip to content

Commit 6eae52b

Browse files
committed
feat: database imrovements
1 parent 4171a96 commit 6eae52b

16 files changed

Lines changed: 284 additions & 30 deletions

File tree

CLAUDE.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is a **monorepo** for the FastAPI Startkit ecosystem — a Laravel/Masonite-inspired framework for building Python applications with FastAPI. It contains four main components:
8+
9+
| Directory | Purpose | Published as |
10+
|---|---|---|
11+
| `fastapi_startkit/` | Core framework package | [`fastapi-startkit`](https://pypi.org/project/fastapi-startkit/) on PyPI |
12+
| `fastapi_startkit.github.io.git/` | Documentation site | GitHub Pages (VitePress) |
13+
| `example/` | Standalone example apps | Not published — reference only |
14+
| `application/` | Starter application template | Not published — clone/scaffold target |
15+
16+
### `fastapi_startkit/` — Core Package
17+
18+
The PyPI package (`fastapi-startkit`, currently v0.13.6). Source lives under `src/fastapi_startkit/`. This is the foundational framework all other components depend on.
19+
20+
**Do not modify framework code unless explicitly necessary.** Changes to core abstractions (Container, Application, Model, Provider, Facades) can have broad breaking effects on downstream applications.
21+
22+
Optional extras are installed with pip/uv extras:
23+
24+
```
25+
fastapi-startkit[fastapi] # FastAPI + Starlette
26+
fastapi-startkit[database] # SQLAlchemy async ORM
27+
fastapi-startkit[postgres] # asyncpg driver
28+
fastapi-startkit[sqlite] # aiosqlite driver
29+
fastapi-startkit[mysql] # aiomysql driver
30+
fastapi-startkit[vite] # Jinja2 for Vite integration
31+
```
32+
33+
### `fastapi_startkit.github.io.git/` — Documentation
34+
35+
VitePress site. Docs cover getting started, configuration, console, database, logging, FastAPI integration, frontend, and exception handling. Edit `.md` files under `docs/` and the home page at `index.md`.
36+
37+
### `example/` — Example Applications
38+
39+
Self-contained apps demonstrating specific features. Each subdirectory is an independent uv workspace member:
40+
41+
| App | What it shows |
42+
|---|---|
43+
| `config-app/` | Configuration system |
44+
| `console-app/` | CLI / Cleo commands |
45+
| `database-app/` | ORM, migrations, seeders |
46+
| `fastapi-app/` | Minimal FastAPI setup |
47+
| `inertia-pingcrm-app/` | Full Inertia.js + PingCRM clone |
48+
| `onefile-app/` | Single-file application |
49+
| `vite-app/` | Vite + Jinja2 frontend |
50+
51+
### `application/` — Starter Application
52+
53+
The template users clone when starting a new project. Contains the minimal scaffolding: `artisan` entrypoint, `bootstrap/`, `config/`, `providers/`, `routes/`, and `storage/`. It mirrors a typical project layout and is a uv workspace member of this monorepo.
54+
55+
## Commands
56+
57+
```bash
58+
# Install all workspace dependencies
59+
uv sync
60+
61+
# Build the core package
62+
cd fastapi_startkit && uv build
63+
64+
# Run framework tests
65+
uv run pytest fastapi_startkit/src/fastapi_startkit/tests/ -v
66+
67+
# Run a single test file
68+
uv run pytest fastapi_startkit/src/fastapi_startkit/tests/configurations/test_config_merge.py -v
69+
70+
# Serve the docs locally
71+
cd fastapi_startkit.github.io.git && npm run dev
72+
```
73+
74+
Tests run with `asyncio_mode = "auto"` (configured in `pyproject.toml`), so all tests are async-capable by default.
75+
76+
## Architecture (Core Package)
77+
78+
### Application Lifecycle
79+
80+
1. `Application(base_path)` initializes the service container and singleton
81+
2. `.load_environment()` loads `.env` + `.env.{APP_ENV}` (auto-detects `.env.testing` under pytest)
82+
3. `.configure_paths()` sets config/storage paths
83+
4. `.register_providers()``.load_providers()` (two-phase boot)
84+
5. `app.fastapi` is lazy-loaded; HTTP routes delegate to the FastAPI instance
85+
86+
### Service Container (`container/container.py`)
87+
88+
Central IoC container. Core API:
89+
- `bind(key, value)` — register a binding
90+
- `make(key)` — resolve a binding
91+
- `resolve(obj)` — auto-wire a callable by inspecting its type-hinted parameters
92+
93+
Hooks (`on_bind`, `on_make`, `on_resolve`) allow intercepting container operations. `collect('Auth*')` returns all bindings matching a wildcard.
94+
95+
### Configuration (`configuration/`)
96+
97+
Define config as a dataclass with fields sourced from environment variables via `env()`:
98+
99+
```python
100+
from dataclasses import dataclass, field
101+
from fastapi_startkit.environment import env
102+
103+
@dataclass
104+
class RedisConfig:
105+
host: str = field(default_factory=lambda: env('REDIS_HOST'))
106+
port: int = field(default_factory=lambda: env('REDIS_PORT'))
107+
```
108+
109+
`app.load_environment()` applies a two-step merge: `.env` as base, then `.env.{APP_ENV}` on top.
110+
111+
Register in the container for dotted-key access:
112+
113+
```python
114+
config = app.make('config')
115+
config.set('redis', RedisConfig())
116+
117+
Config.get('redis.host') # via facade
118+
```
119+
120+
### Provider Pattern (`providers/`)
121+
122+
Providers are the standard way to register services. Each provider has two phases:
123+
- `register()` — bind things into the container
124+
- `boot()` — run after all providers are registered (safe to resolve dependencies here)
125+
126+
### FastAPI Routing (`fastapi/routers/router.py`)
127+
128+
`Router` wraps FastAPI's `APIRouter` and adds a `resource()` shortcut.
129+
130+
```python
131+
from fastapi_startkit.fastapi import Router
132+
133+
router = Router()
134+
router.get("/path", endpoint)
135+
router.post("/path", endpoint)
136+
router.put("/path", endpoint)
137+
router.patch("/path", endpoint)
138+
router.delete("/path", endpoint)
139+
```
140+
141+
`router.resource(name, controller)` registers standard CRUD routes (index, create, store, show, edit, update, destroy). Use `only=`, `excepts=`, `names=`, `parameters=` to customise.
142+
143+
Group routes by access level using separate `Router` instances:
144+
145+
```python
146+
# routes/web.py
147+
from fastapi import Depends
148+
from fastapi_startkit.fastapi import Router
149+
150+
guest = Router()
151+
guest.get("/login", auth_controller.create)
152+
guest.post("/login", auth_controller.store)
153+
154+
auth = Router(dependencies=[Depends(auth_middleware)])
155+
auth.get("/", dashboard_controller.index)
156+
auth.resource("users", users_controller)
157+
```
158+
159+
### ORM (`masoniteorm/`)
160+
161+
Async-first fork of Masonite ORM built on SQLAlchemy async:
162+
- All DB operations are `async`/`await`
163+
- `Model` auto-pluralizes table names via `inflection`
164+
- `created_at`/`updated_at` managed as `pendulum` Carbon objects
165+
- Relationships: `HasOne`, `HasMany`, `BelongsTo`, `BelongsToMany`, `HasOneThrough`
166+
- `AsyncQueryBuilder` provides the chainable query interface
167+
168+
### Facades (`facades/`)
169+
170+
Static-like access to container-resolved services (`Config.get()`, `Auth.user()`, etc.). Each facade has a `.pyi` stub for IDE type support. Requires a booted Application singleton.
171+
172+
### Console (`commands/`, `masoniteorm/commands/`)
173+
174+
CLI built on [Cleo](https://github.com/python-poetry/cleo). Database commands (migrate, seed, make:model, etc.) live in `masoniteorm/commands/`. Run via `uv run artisan`.
175+
176+
## Key Dependencies
177+
178+
| Package | Purpose |
179+
|---|---|
180+
| `fastapi[standard]` | HTTP framework (lazily imported) |
181+
| `sqlalchemy[asyncio]` | Async ORM backend |
182+
| `pendulum` | Datetime/timezone (used as Carbon) |
183+
| `cleo` | CLI commands |
184+
| `dotty-dict` | Nested dict access via dotted keys |
185+
| `inflection` | Table name pluralization |
186+
| `asyncpg` / `aiomysql` / `aiosqlite` | DB drivers |

fastapi_startkit/src/fastapi_startkit/application.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import os
2+
from fastapi_startkit.providers.app_provider import AppProvider
23
from pathlib import Path
34
from typing import TYPE_CHECKING, Optional
45
from typing import Type, Callable, Any, List, TypeVar, Generic
56

6-
from fastapi_startkit.providers.app_provider import AppProvider
77
from .config import AppConfig
88
from .configuration.providers import ConfigurationProvider
99
from .container import Container
@@ -30,12 +30,12 @@ class Application(Container, Generic[TConfig]):
3030
]
3131

3232
def __init__(
33-
self,
34-
base_path: str | Path = None,
35-
env=None,
36-
providers=None,
37-
config: Type[TConfig] | None = None,
38-
exception_handler: Type[ExceptionHandler] | None = None,
33+
self,
34+
base_path: str | Path = None,
35+
env=None,
36+
providers=None,
37+
config: Type[TConfig] | None = None,
38+
exception_handler: Type[ExceptionHandler] | None = None,
3939
):
4040
super().__init__()
4141

@@ -83,6 +83,9 @@ def register_providers(self):
8383
config = {}
8484
if isinstance(provider_data, tuple):
8585
provider_class, config = provider_data
86+
87+
if callable(config):
88+
config = config()
8689
else:
8790
provider_class = provider_data
8891

@@ -151,7 +154,7 @@ def mount(self, path: str, app_instance: "FastAPI", **kwargs):
151154

152155
# Add custom exception handlers
153156
def add_exception_handler(
154-
self, exc_class_or_status_code: Any, handler: Callable[..., Any]
157+
self, exc_class_or_status_code: Any, handler: Callable[..., Any]
155158
):
156159
self._fastapi.add_exception_handler(exc_class_or_status_code, handler)
157160
return self
@@ -178,9 +181,9 @@ def load_environment(self):
178181

179182
def is_debug(self) -> bool:
180183
return (
181-
hasattr(self, "_config_instance")
182-
and self._config_instance is not None
183-
and getattr(self._config_instance, "debug", False)
184+
hasattr(self, "_config_instance")
185+
and self._config_instance is not None
186+
and getattr(self._config_instance, "debug", False)
184187
)
185188

186189
def configure_config(self):
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
from .app import AppConfig
2-
from .facades import Config

fastapi_startkit/src/fastapi_startkit/exceptions/handler.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@ def report(self, exception: Exception):
6262
self.report_exception(exception)
6363

6464
def report_exception(self, exception: Exception):
65+
context = self._build_context(exception)
6566
if self.app and self.app.has("logger"):
6667
from fastapi_startkit.logging.logger import Logger
6768

68-
Logger.error(self._build_context(exception))
69+
Logger.error(context)
70+
else:
71+
print(context, file=sys.stderr)
6972

7073
def _build_context(self, exception: Exception) -> str:
7174
import traceback

fastapi_startkit/src/fastapi_startkit/helpers/string.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ def trim(self, suffix: str) -> "Stringable":
1414
def slugify(self) -> "Stringable":
1515
return Stringable(Str.slugify(self.text))
1616

17+
def camel_case(self) -> "Stringable":
18+
return Stringable(Str.camel_case(self.text))
19+
20+
def snake_case(self) -> "Stringable":
21+
return Stringable(Str.snake_case(self.text))
22+
1723

1824
class Str:
1925
@classmethod
@@ -29,3 +35,17 @@ def slugify(cls, text: str) -> str:
2935
def trim(cls, text: str, word: str) -> str:
3036
"""Remove all occurrences of a word from the string (case-insensitive)."""
3137
return re.sub(re.escape(word), "", text, flags=re.IGNORECASE).strip("_").strip()
38+
39+
@classmethod
40+
def camel_case(cls, text: str) -> str:
41+
"""Convert a string to camelCase."""
42+
words = re.split(r"[-_\s]+", text)
43+
return words[0].lower() + "".join(word.capitalize() for word in words[1:])
44+
45+
@classmethod
46+
def snake_case(cls, text: str) -> str:
47+
"""Convert a string to snake_case."""
48+
text = re.sub(r"[-\s]+", "_", text)
49+
text = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", text)
50+
text = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", text)
51+
return text.lower()

fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MakeMigrationCommand.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import datetime
22
import os
33
import pathlib
4-
from inflection import camelize, tableize
4+
from inflection import tableize
55
from cleo.helpers import argument, option
6+
from fastapi_startkit.helpers.string import Str
67
from .Command import Command
78

89

@@ -29,7 +30,7 @@ class MakeMigrationCommand(Command):
2930
]
3031

3132
def handle(self):
32-
name = self.argument("name")
33+
name = self.argument("name").replace("-", "_")
3334
now = datetime.datetime.today()
3435

3536
if self.option("create") != "None":
@@ -52,7 +53,9 @@ def handle(self):
5253
)
5354
) as fp:
5455
output = fp.read()
55-
output = output.replace("__MIGRATION_NAME__", camelize(name))
56+
camel = Str.camel_case(name)
57+
class_name = camel[0].upper() + camel[1:]
58+
output = output.replace("__MIGRATION_NAME__", class_name)
5659
output = output.replace("__TABLE_NAME__", table)
5760

5861
file_name = f"{now.strftime('%Y_%m_%d_%H%M%S')}_{name}.py"

fastapi_startkit/src/fastapi_startkit/masoniteorm/commands/MigrateFreshCommand.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class MigrateFreshCommand(Command):
2222
description="The location of the migration directory",
2323
),
2424
option(
25-
"ignore-fk", "i", flag=True, description="Ignore foreign key constraints"
25+
"no-fk", None, flag=True, description="Re-enable foreign key constraints during drop"
2626
),
2727
option(
2828
"seed",
@@ -54,7 +54,7 @@ async def handle_async(self):
5454
migration_directory=self.option("directory"),
5555
)
5656

57-
await migration.fresh(ignore_fk=self.option("ignore-fk"))
57+
await migration.fresh(ignore_fk=not self.option("no-fk"))
5858

5959
if self.option("seed") == "null":
6060
self.call(
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
"""__MIGRATION_NAME__ Migration."""
22

3-
from masoniteorm.migrations import Migration
3+
from fastapi_startkit.masoniteorm.migrations import Migration
44

55

66
class __MIGRATION_NAME__(Migration):
7-
def up(self):
7+
async def up(self):
88
"""
99
Run the migrations.
1010
"""
11-
with self.schema.create("__TABLE_NAME__") as table:
11+
async with await self.schema.create("__TABLE_NAME__") as table:
1212
table.increments("id")
1313

1414
table.timestamps()
1515

16-
def down(self):
16+
async def down(self):
1717
"""
1818
Revert the migrations.
1919
"""
20-
self.schema.drop("__TABLE_NAME__")
20+
await self.schema.drop("__TABLE_NAME__")
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
"""__MIGRATION_NAME__ Migration."""
22

3-
from masoniteorm.migrations import Migration
3+
from fastapi_startkit.masoniteorm.migrations import Migration
44

55

66
class __MIGRATION_NAME__(Migration):
7-
def up(self):
7+
async def up(self):
88
"""
99
Run the migrations.
1010
"""
11-
with self.schema.table("__TABLE_NAME__") as table:
11+
async with await self.schema.table("__TABLE_NAME__") as table:
1212
pass
1313

14-
def down(self):
14+
async def down(self):
1515
"""
1616
Revert the migrations.
1717
"""
18-
with self.schema.table("__TABLE_NAME__") as table:
18+
async with await self.schema.table("__TABLE_NAME__") as table:
1919
pass

0 commit comments

Comments
 (0)