Skip to content

Commit 64fc436

Browse files
authored
Merge pull request #34 from fastapi-startkit/database-orm-improvements
feat: database imrovements
2 parents acb5eee + 2a2c95a commit 64fc436

49 files changed

Lines changed: 338 additions & 78 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.githooks/pre-commit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/sh
2+
cd fastapi_startkit && uvx ruff check --fix src/

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 |
Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,23 @@
11
from pathlib import Path
22

3-
from config.database import DatabaseConfig
4-
from config.logging import LoggingConfig
5-
from providers.console_provider import ConsoleProvider
6-
from providers.fastapi_provider import FastAPIServiceProvider
7-
8-
from config.app import AppConfig
9-
10-
print("Loading Application class...")
113
from fastapi_startkit.application import Application
124
from fastapi_startkit.exceptions import ExceptionHandler
135
from fastapi_startkit.logging.providers import LogProvider
146
from fastapi_startkit.masoniteorm.providers import DatabaseProvider
157

16-
17-
class _FallbackHandler:
18-
async def render(self, request, exc):
19-
from fastapi.responses import JSONResponse
20-
return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
8+
from config.app import AppConfig
9+
from config.database import DatabaseConfig
10+
from config.logging import LoggingConfig
11+
from providers.console_provider import ConsoleProvider
12+
from providers.fastapi_provider import FastAPIServiceProvider
2113

2214

2315
class AppExceptionHandler(ExceptionHandler):
2416
def register(self):
25-
self.register_handler(Exception, _FallbackHandler())
26-
17+
pass
2718

2819
app: Application[AppConfig] = Application(
29-
base_path=str(Path().cwd()),
20+
base_path=Path(__file__).parent.parent,
3021
config=AppConfig,
3122
providers=[
3223
(LogProvider, LoggingConfig),
@@ -35,4 +26,4 @@ def register(self):
3526
FastAPIServiceProvider,
3627
],
3728
exception_handler=AppExceptionHandler,
38-
)
29+
)

example/database-app/routes/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
public = Router()
77

88
public.post("/register/student", student_auth.register)
9-
public.post("/register/teacher", AuthController.register_teacher)
9+
public.post("/register/teacher", AuthController.register_teacher)

fastapi_startkit/pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ dev = [
5050
]
5151

5252

53+
[tool.ruff]
54+
line-length = 120
55+
56+
[tool.ruff.lint]
57+
select = ["F401"]
58+
fixable = ["F401"]
59+
60+
[tool.ruff.lint.per-file-ignores]
61+
"__init__.py" = ["F401"]
62+
5363
[tool.pytest.ini_options]
5464
asyncio_mode = "auto"
5565

fastapi_startkit/src/fastapi_startkit/application.py

Lines changed: 4 additions & 1 deletion
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
@@ -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

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/configuration/Configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi_startkit.loader import Loader
22
from ..utils.structures import data
3-
from ..exceptions import InvalidConfigurationLocation, InvalidConfigurationSetup
3+
from ..exceptions import InvalidConfigurationSetup
44

55

66
class Configuration:

fastapi_startkit/src/fastapi_startkit/exceptions/handler.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import atexit
33
from typing import Any, Callable, Dict, List, Optional, Type
44

5-
from dumpdie import dd
65

76

87
class ExceptionHandler:
@@ -62,10 +61,13 @@ def report(self, exception: Exception):
6261
self.report_exception(exception)
6362

6463
def report_exception(self, exception: Exception):
64+
context = self._build_context(exception)
6565
if self.app and self.app.has("logger"):
6666
from fastapi_startkit.logging.logger import Logger
6767

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

7072
def _build_context(self, exception: Exception) -> str:
7173
import traceback
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
class HTTPExceptionHandler:
2+
"""
3+
The base exception handler for FastAPI applications.
4+
"""
5+
6+
async def render(self, request, exc):
7+
import traceback
8+
from fastapi.responses import JSONResponse
9+
from fastapi_startkit.container import Container
10+
11+
app = Container.instance()
12+
if app.is_debug():
13+
tb = exc.__traceback__
14+
frames = traceback.extract_tb(tb)
15+
content = {
16+
"message": str(exc),
17+
"exception": f"{type(exc).__module__}.{type(exc).__qualname__}",
18+
"file": frames[-1].filename if frames else None,
19+
"line": frames[-1].lineno if frames else None,
20+
"trace": [
21+
{"file": f.filename, "line": f.lineno, "function": f.name}
22+
for f in frames
23+
],
24+
}
25+
else:
26+
content = {"message": "Server Error"}
27+
28+
return JSONResponse(status_code=500, content=content)

0 commit comments

Comments
 (0)