Skip to content

Commit b56132a

Browse files
committed
Load proj from db conn string
1 parent 1436b0d commit b56132a

9 files changed

Lines changed: 407 additions & 58 deletions

File tree

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ test:
1515
uv run pytest tests -s -v
1616

1717
test-filter:
18-
uv run pytest tests -v -s -k $(filter)
18+
uv run pytest tests -v -s -k $(filter)
19+
20+
db:
21+
docker compose up

docker-compose.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
services:
2+
postgres:
3+
image: postgres:13.8-bullseye
4+
hostname: forge
5+
container_name: forge
6+
environment:
7+
POSTGRES_PASSWORD: forge
8+
POSTGRES_USER: forge
9+
POSTGRES_DB: forge
10+
volumes:
11+
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
12+
ports:
13+
- "5432:5432"
14+
restart: always
15+
healthcheck:
16+
test: pg_isready -U forge
17+
interval: 2s
18+
timeout: 3s
19+
retries: 40

fastapi_forge/__main__.py

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import click
44

55
from fastapi_forge.frontend import init
6+
from fastapi_forge.project_io import ProjectLoader
67

78

89
@click.group()
@@ -14,44 +15,52 @@ def main() -> None:
1415
@click.option(
1516
"--use-example",
1617
is_flag=True,
17-
help="Generate a new project using a prebuilt example provided by FastAPI Forge. "
18-
"This option is ideal for quickly getting started with a standard template.",
18+
help="Generate a new project using a prebuilt example provided by FastAPI Forge.",
1919
)
2020
@click.option(
2121
"--no-ui",
2222
is_flag=True,
23-
help="Generate the project directly in the terminal without launching the UI. "
24-
"Use this option for headless environments or when you prefer a CLI-only workflow.",
23+
help="Generate the project directly in the terminal without launching the UI.",
2524
)
2625
@click.option(
2726
"--from-yaml",
2827
type=click.Path(exists=True, dir_okay=False, readable=True),
29-
help="Generate a project using a custom configuration from a YAML file. "
30-
"Provide the path to the YAML file (supports relative or absolute paths, and '~' for home directory). "
31-
"Use '--no-ui' to generate the project immediately, otherwise the configuration will be loaded into the UI for further customization.",
28+
help="Generate a project using a custom configuration from a YAML file.",
3229
)
33-
def start(use_example: bool, no_ui: bool, from_yaml: str | None = None) -> None:
30+
@click.option(
31+
"--db-url",
32+
help="PostgreSQL connection URL (e.g., postgresql://user:password@host:port/dbname)",
33+
)
34+
def start(
35+
use_example: bool,
36+
no_ui: bool,
37+
from_yaml: str | None = None,
38+
db_url: str | None = None,
39+
) -> None:
3440
"""Start the FastAPI Forge server and generate a new project."""
35-
if use_example and from_yaml:
36-
msg = "Cannot use '--use-example' and '--from-yaml' together."
37-
raise click.UsageError(
38-
msg,
39-
)
41+
if sum([use_example, bool(from_yaml), bool(db_url)]) > 1:
42+
msg = "Only one of '--use-example', '--from-yaml', or '--db-url' can be used."
43+
raise click.UsageError(msg)
4044

41-
yaml_path = None
42-
if from_yaml:
43-
yaml_path = Path.expanduser(Path(from_yaml)).resolve()
45+
project_spec = None
4446

45-
if not yaml_path.exists():
46-
raise click.FileError(f"YAML file not found: {yaml_path}")
47+
if from_yaml:
48+
yaml_path = Path(from_yaml).expanduser().resolve()
4749
if not yaml_path.is_file():
48-
raise click.FileError(f"Path is not a file: {yaml_path}")
50+
raise click.FileError(f"YAML file not found: {yaml_path}")
51+
project_spec = ProjectLoader(project_path=yaml_path).load_project_input()
52+
53+
elif db_url:
54+
project_spec = ProjectLoader.load_project_spec_from_db(
55+
connection_string=db_url,
56+
)
57+
58+
else:
59+
base_path = Path(__file__).parent / "example-projects"
60+
path = base_path / ("game_zone.yaml" if use_example else "empty-service.yaml")
61+
project_spec = ProjectLoader(project_path=path).load_project_input()
4962

50-
init(
51-
use_example=use_example,
52-
no_ui=no_ui,
53-
yaml_path=yaml_path,
54-
)
63+
init(project_spec=project_spec, no_ui=no_ui)
5564

5665

5766
@main.command()

fastapi_forge/enums.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@ def as_python_type(self) -> str:
2121
FieldDataType.JSONB: "dict[str, Any]",
2222
}[self]
2323

24+
@classmethod
25+
def from_db_type(cls, db_type: str) -> "FieldDataType":
26+
db_type = db_type.lower()
27+
match db_type:
28+
case _ if db_type.startswith("character varying") or db_type == "text":
29+
return cls.STRING
30+
case "integer" | "bigint" | "smallint":
31+
return cls.INTEGER
32+
case "numeric":
33+
return cls.FLOAT
34+
case "boolean":
35+
return cls.BOOLEAN
36+
case "uuid":
37+
return cls.UUID
38+
case _ if db_type.startswith("timestamp") or "date":
39+
return cls.DATETIME
40+
case "jsonb":
41+
return cls.JSONB
42+
case _:
43+
raise ValueError(f"Unsupported database type: {db_type}")
44+
2445

2546
class HTTPMethod(StrEnum):
2647
GET = "get"

fastapi_forge/frontend/main.py

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import asyncio
2-
from pathlib import Path
32

43
from nicegui import native, ui
54

6-
from fastapi_forge import project_io as p
5+
from fastapi_forge.dtos import ProjectSpec
76
from fastapi_forge.forge import build_project
87
from fastapi_forge.frontend import (
98
Header,
@@ -14,12 +13,6 @@
1413
from fastapi_forge.frontend.state import state
1514

1615

17-
async def _init_no_ui(project_path: Path) -> None:
18-
"""Initialize project without UI"""
19-
project_spec = p.ProjectLoader(project_path).load_project_spec()
20-
await build_project(project_spec)
21-
22-
2316
def setup_ui() -> None:
2417
"""Setup basic UI configuration"""
2518
ui.add_head_html(
@@ -30,11 +23,6 @@ def setup_ui() -> None:
3023
Header()
3124

3225

33-
def load_initial_project(path: Path) -> p.ProjectSpec:
34-
"""Load project specification from file"""
35-
return p.ProjectLoader(project_path=path).load_project_input()
36-
37-
3826
def create_ui_components() -> None:
3927
"""Create all UI components"""
4028
with ui.column().classes("w-full h-full items-center justify-center mt-4"):
@@ -57,30 +45,20 @@ def run_ui(reload: bool) -> None:
5745

5846
def init(
5947
reload: bool = False,
60-
use_example: bool = False,
6148
no_ui: bool = False,
62-
yaml_path: Path | None = None,
49+
project_spec: ProjectSpec | None = None,
6350
) -> None:
64-
"""Main initialization function"""
65-
base_path = Path(__file__).parent.parent / "example-projects"
66-
default_path = base_path / "empty-service.yaml"
67-
example_path = base_path / "game_zone.yaml"
51+
if project_spec:
52+
if no_ui:
53+
asyncio.run(build_project(project_spec))
54+
return
6855

69-
path = example_path if use_example else yaml_path if yaml_path else default_path
70-
71-
if no_ui:
72-
asyncio.run(_init_no_ui(path))
73-
return
56+
state.initialize_from_project(project_spec)
7457

7558
setup_ui()
76-
77-
if use_example or yaml_path:
78-
initial_project = load_initial_project(path)
79-
state.initialize_from_project(initial_project)
80-
8159
create_ui_components()
8260
run_ui(reload)
8361

8462

8563
if __name__ in {"__main__", "__mp_main__"}:
86-
init(reload=True, use_example=False)
64+
init(reload=True)

0 commit comments

Comments
 (0)