Skip to content

Commit 59cd7b5

Browse files
authored
Improve liniting (#10)
* Init * Remaining
1 parent 55c755b commit 59cd7b5

29 files changed

Lines changed: 553 additions & 366 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ version:
99

1010
lint:
1111
uv run ruff format
12-
uv run ruff check . --fix
12+
uv run ruff check . --fix --unsafe-fixes
1313

1414
test:
1515
uv run pytest tests -s

fastapi_forge/__main__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
from pathlib import Path
2+
13
import click
4+
25
from fastapi_forge.frontend import init
3-
from pathlib import Path
4-
import os
56

67

78
@click.group()
@@ -32,14 +33,14 @@ def main() -> None:
3233
def start(use_example: bool, no_ui: bool, from_yaml: str | None = None) -> None:
3334
"""Start the FastAPI Forge server and generate a new project."""
3435
if use_example and from_yaml:
36+
msg = "Cannot use '--use-example' and '--from-yaml' together."
3537
raise click.UsageError(
36-
"Cannot use '--use-example' and '--from-yaml' together.",
38+
msg,
3739
)
3840

3941
yaml_path = None
4042
if from_yaml:
41-
expanded_yaml_path = os.path.expanduser(from_yaml)
42-
yaml_path = Path(expanded_yaml_path).resolve()
43+
yaml_path = Path.expanduser(Path(from_yaml)).resolve()
4344

4445
if not yaml_path.exists():
4546
raise click.FileError(f"YAML file not found: {yaml_path}")

fastapi_forge/dtos.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1+
from typing import Annotated, Self
2+
13
from pydantic import (
24
BaseModel,
3-
computed_field,
5+
ConfigDict,
46
Field,
5-
model_validator,
7+
computed_field,
68
field_validator,
7-
ConfigDict,
9+
model_validator,
810
)
9-
from typing import Annotated
11+
1012
from fastapi_forge.enums import FieldDataType
11-
from typing_extensions import Self
12-
from fastapi_forge.string_utils import snake_to_camel, camel_to_snake_hyphen
13+
from fastapi_forge.string_utils import camel_to_snake_hyphen, snake_to_camel
1314

1415
BoundedStr = Annotated[str, Field(..., min_length=1, max_length=100)]
1516
SnakeCaseStr = Annotated[BoundedStr, Field(..., pattern=r"^[a-z][a-z0-9_]*$")]
1617
ModelName = SnakeCaseStr
1718
FieldName = SnakeCaseStr
1819
BackPopulates = Annotated[str, Field(..., pattern=r"^[a-z][a-z0-9_]*$")]
1920
ProjectName = Annotated[
20-
BoundedStr, Field(..., pattern=r"^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$")
21+
BoundedStr,
22+
Field(..., pattern=r"^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$"),
2123
]
2224

2325

@@ -55,19 +57,23 @@ def _validate(self) -> Self:
5557
"""Validate field constraints."""
5658
if self.primary_key:
5759
if self.nullable:
58-
raise ValueError("Primary key cannot be nullable.")
60+
msg = "Primary key cannot be nullable."
61+
raise ValueError(msg)
5962
if not self.unique:
6063
self.unique = True
6164

6265
metadata = self.metadata
63-
if metadata.is_created_at_timestamp or metadata.is_updated_at_timestamp:
64-
if self.type != FieldDataType.DATETIME:
65-
raise ValueError(
66-
"Create/update timestamp fields must be of type DateTime."
67-
)
66+
if (
67+
metadata.is_created_at_timestamp or metadata.is_updated_at_timestamp
68+
) and self.type != FieldDataType.DATETIME:
69+
msg = "Create/update timestamp fields must be of type DateTime."
70+
raise ValueError(
71+
msg,
72+
)
6873

6974
if metadata.is_foreign_key and self.type != FieldDataType.UUID:
70-
raise ValueError("Foreign Keys must be of type UUID.")
75+
msg = "Foreign Keys must be of type UUID."
76+
raise ValueError(msg)
7177
return self
7278

7379
@computed_field
@@ -112,7 +118,8 @@ class ModelRelationship(_Base):
112118
def _validate_field_name(cls, value: str) -> str:
113119
"""Ensure relationship field names end with '_id'."""
114120
if not value.endswith("_id"):
115-
raise ValueError("Relationship field names must end with '_id'.")
121+
msg = "Relationship field names must end with '_id'."
122+
raise ValueError(msg)
116123
return value
117124

118125
@computed_field
@@ -189,7 +196,7 @@ def _validate(self) -> Self:
189196
]
190197
if len(unque_relationships) != len(set(unque_relationships)):
191198
raise ValueError(
192-
f"Model '{self.name}' contains duplicate relationship field names."
199+
f"Model '{self.name}' contains duplicate relationship field names.",
193200
)
194201

195202
return self
@@ -232,7 +239,7 @@ def _validate_metadata(self) -> Self:
232239
]
233240
if missing:
234241
error_message = rule["error_message"].format(
235-
missing=", ".join(missing)
242+
missing=", ".join(missing),
236243
)
237244
raise ValueError(error_message)
238245

@@ -251,7 +258,7 @@ def get_preview(self) -> "Model":
251258
unique=relation.unique,
252259
index=relation.index,
253260
metadata=ModelFieldMetadata(is_foreign_key=True),
254-
)
261+
),
255262
)
256263

257264
return preview_model
@@ -273,20 +280,23 @@ def _validate_models(self) -> Self:
273280
model_names = [model.name for model in self.models]
274281
model_names_set = set(model_names)
275282
if len(model_names) != len(model_names_set):
276-
raise ValueError("Model names must be unique.")
283+
msg = "Model names must be unique."
284+
raise ValueError(msg)
277285

278286
if self.use_alembic and not self.use_postgres:
279-
raise ValueError("Cannot use Alembic if PostgreSQL is not enabled.")
287+
msg = "Cannot use Alembic if PostgreSQL is not enabled."
288+
raise ValueError(msg)
280289

281290
if self.use_builtin_auth and not self.use_postgres:
282-
raise ValueError("Cannot use built-in auth if PostgreSQL is not enabled.")
291+
msg = "Cannot use built-in auth if PostgreSQL is not enabled."
292+
raise ValueError(msg)
283293

284294
for model in self.models:
285295
for relationship in model.relationships:
286296
if relationship.target_model not in model_names_set:
287297
raise ValueError(
288298
f"Model '{model.name}' has a relationship to "
289-
f"'{relationship.target_model}', which does not exist."
299+
f"'{relationship.target_model}', which does not exist.",
290300
)
291301

292302
return self
@@ -324,7 +334,7 @@ def has_cycle(node):
324334
if has_cycle(model_name):
325335
raise ValueError(
326336
f"Circular reference detected involving model '{model_name}'. "
327-
"Remove bidirectional relationships between models."
337+
"Remove bidirectional relationships between models.",
328338
)
329339

330340
return self

fastapi_forge/forge.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
1-
import os
2-
import shutil
31
import asyncio
2+
import shutil
3+
from pathlib import Path
4+
from time import perf_counter
5+
46
from cookiecutter.main import cookiecutter
7+
58
from fastapi_forge.dtos import ProjectSpec
69
from fastapi_forge.logger import logger
710
from fastapi_forge.project_io import ProjectBuilder
8-
from time import perf_counter
911

1012

11-
def _get_template_path() -> str:
13+
def _get_template_path() -> Path:
1214
"""Return the absolute path to the project template directory."""
13-
template_path = os.path.join(os.path.dirname(__file__), "template")
14-
if not os.path.exists(template_path):
15+
template_path = Path(__file__).parent / "template"
16+
if not template_path.exists():
1517
raise RuntimeError(f"Template directory not found: {template_path}")
1618
return template_path
1719

1820

1921
async def _teardown_project(project_name: str) -> None:
2022
"""Forcefully remove the project directory and all its contents."""
21-
project_dir = os.path.join(os.getcwd(), project_name)
22-
if os.path.exists(project_dir):
23+
project_dir = Path.cwd() / project_name
24+
if project_dir.exists():
2325
await asyncio.to_thread(shutil.rmtree, project_dir)
2426
logger.info(f"Removed project directory: {project_dir}")
2527

@@ -33,10 +35,11 @@ async def build_project(spec: ProjectSpec) -> None:
3335
builder = ProjectBuilder(spec)
3436
await builder.build_artifacts()
3537

36-
template_path = _get_template_path()
38+
template_path = str(_get_template_path())
39+
3740
cookiecutter(
3841
template_path,
39-
output_dir=os.getcwd(),
42+
output_dir=str(Path.cwd()),
4043
no_input=True,
4144
overwrite_if_exists=True,
4245
extra_context={
@@ -50,7 +53,7 @@ async def build_project(spec: ProjectSpec) -> None:
5053

5154
end = perf_counter()
5255
logger.info(f"Project built in {end - start:.2f} seconds.")
53-
except Exception as e:
54-
logger.error(f"Failed to create project: {e}")
56+
except Exception as exc:
57+
logger.error(f"Failed to create project: {exc}")
5558
await _teardown_project(spec.project_name)
5659
raise

fastapi_forge/frontend/components/header.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ def _build(self) -> None:
1313
icon="eva-github",
1414
color="white",
1515
on_click=lambda: ui.navigate.to(
16-
"https://github.com/mslaursen/fastapi-forge"
16+
"https://github.com/mslaursen/fastapi-forge",
1717
),
1818
).classes("self-center", remove="bg-white").tooltip(
19-
"Drop a ⭐️ if you like FastAPI Forge!"
19+
"Drop a ⭐️ if you like FastAPI Forge!",
2020
)
2121

2222
ui.label(text="FastAPI Forge").classes(
23-
"font-bold ml-auto self-center text-2xl"
23+
"font-bold ml-auto self-center text-2xl",
2424
)
2525

2626
ui.button(

fastapi_forge/frontend/components/model_create.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from nicegui import ui
2+
23
from fastapi_forge.frontend.state import state
34

45

@@ -13,7 +14,7 @@ def _build(self) -> None:
1314
ui.input(placeholder="Model name")
1415
.classes("self-center")
1516
.tooltip(
16-
"Model names should be singular (e.g., 'user' instead of 'users')."
17+
"Model names should be singular (e.g., 'user' instead of 'users').",
1718
)
1819
)
1920
self.add_button = (

fastapi_forge/frontend/components/model_row.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from nicegui import ui
2+
23
from fastapi_forge.dtos import Model
34
from fastapi_forge.frontend.state import state
45

@@ -31,10 +32,12 @@ def _build(self) -> None:
3132

3233
with ui.row().classes("gap-2"):
3334
self.edit_button = ui.button(
34-
icon="edit", on_click=self._toggle_edit
35+
icon="edit",
36+
on_click=self._toggle_edit,
3537
).bind_visibility_from(self, "is_editing", lambda x: not x)
3638
self.save_button = ui.button(
37-
icon="save", on_click=self._save_model
39+
icon="save",
40+
on_click=self._save_model,
3841
).bind_visibility_from(self, "is_editing")
3942
ui.button(icon="delete", on_click=self._delete_model)
4043

fastapi_forge/frontend/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Any
22

3-
43
FIELD_COLUMNS: list[dict[str, Any]] = [
54
{
65
"name": "name",

fastapi_forge/frontend/main.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
from fastapi_forge.forge import build_project
1+
import asyncio
2+
from pathlib import Path
3+
4+
from nicegui import native, ui
5+
26
from fastapi_forge import project_io as p
7+
from fastapi_forge.forge import build_project
38
from fastapi_forge.frontend import (
49
Header,
5-
ModelPanel,
610
ModelEditorPanel,
11+
ModelPanel,
712
ProjectConfigPanel,
813
)
9-
from pathlib import Path
10-
from nicegui import ui, native
1114
from fastapi_forge.frontend.state import state
12-
import asyncio
1315

1416

1517
async def _init_no_ui(project_path: Path) -> None:
@@ -21,7 +23,7 @@ async def _init_no_ui(project_path: Path) -> None:
2123
def setup_ui() -> None:
2224
"""Setup basic UI configuration"""
2325
ui.add_head_html(
24-
'<link href="https://unpkg.com/eva-icons@1.1.3/style/eva-icons.css" rel="stylesheet" />'
26+
'<link href="https://unpkg.com/eva-icons@1.1.3/style/eva-icons.css" rel="stylesheet" />',
2527
)
2628
ui.button.default_props("round flat dense")
2729
ui.input.default_props("dense")

fastapi_forge/frontend/modals/field_modal.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from collections.abc import Callable
2+
13
from nicegui import ui
2-
from fastapi_forge.enums import FieldDataType
4+
35
from fastapi_forge.dtos import ModelField
4-
from typing import Callable
6+
from fastapi_forge.enums import FieldDataType
57
from fastapi_forge.frontend.state import state
68

79

@@ -18,7 +20,8 @@ def _build(self) -> None:
1820
with ui.row().classes("w-full gap-2"):
1921
self.field_name = ui.input(label="Field Name").classes("w-full")
2022
self.field_type = ui.select(
21-
list(FieldDataType), label="Field Type"
23+
list(FieldDataType),
24+
label="Field Type",
2225
).classes("w-full")
2326
self.primary_key = ui.checkbox("Primary Key").classes("w-full")
2427
self.nullable = ui.checkbox("Nullable").classes("w-full")
@@ -64,15 +67,15 @@ def _build(self) -> None:
6467
ui.label("Update Field").classes("text-lg font-bold")
6568
with ui.row().classes("w-full gap-2"):
6669
self.field_name = ui.input(label="Field Name", value="").classes(
67-
"w-full"
70+
"w-full",
6871
)
6972
self.field_type = ui.select(
7073
list(FieldDataType),
7174
label="Field Type",
7275
value=None,
7376
).classes("w-full")
7477
self.primary_key = ui.checkbox("Primary Key", value=False).classes(
75-
"w-full"
78+
"w-full",
7679
)
7780
self.nullable = ui.checkbox("Nullable", value=False).classes("w-full")
7881
self.unique = ui.checkbox("Unique", value=False).classes("w-full")

0 commit comments

Comments
 (0)