Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions fastapi_forge/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles

from fastapi_forge.core.build import build_fastapi_project
from fastapi_forge.schemas import ProjectSpec

app = FastAPI()


Expand All @@ -22,5 +25,13 @@ def start_forge_api() -> None:
uvicorn.run(app, host="localhost", port=8000)


@app.post("/generate")
async def generate_project(project_spec: ProjectSpec) -> None:
try:
await build_fastapi_project(project_spec, dry_run=False)
except Exception as e:
print(e)


if __name__ == "__main__":
start_forge_api()
1 change: 1 addition & 0 deletions fastapi_forge/static/assets/index-BN-dc6bv.css

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions fastapi_forge/static/assets/index-DWiWI9aR.js

Large diffs are not rendered by default.

46 changes: 0 additions & 46 deletions fastapi_forge/static/assets/index-DuOkOUCX.js

This file was deleted.

1 change: 0 additions & 1 deletion fastapi_forge/static/assets/index-oV0aoT2I.css

This file was deleted.

4 changes: 2 additions & 2 deletions fastapi_forge/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script type="module" crossorigin src="/assets/index-DuOkOUCX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-oV0aoT2I.css">
<script type="module" crossorigin src="/assets/index-DWiWI9aR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BN-dc6bv.css">
</head>
<body>
<div id="app"></div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
[project]
name = "{{cookiecutter.project_name}}"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.8",
"uvicorn>=0.34.0",
"pydantic-settings>=2.7.1",
"pydantic>=2.10.6",
"email-validator>=2.2.0",
"schemahound>=0.1.14",
"loguru>=0.7.3",
"yarl>=1.18.3",
"ruff>=0.9.4",
Expand Down Expand Up @@ -46,11 +50,7 @@ dependencies = [
{%- if cookiecutter.use_logfire -%}
"logfire[aio-pika,asyncpg,fastapi,httpx,sqlalchemy,system-metrics]>=3.16.0",
{%- endif %}
]name = "{{cookiecutter.project_name}}"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
]

[tool.pytest.ini_options]
env = [
Expand All @@ -59,7 +59,7 @@ env = [
]

[tool.ruff]
exclude = ["migrations", ".venv/", "Lib"]
exclude = ["migrations",".venv/", "Lib"]
target-version = "py312"
line-length = 88
indent-width = 4
Expand Down Expand Up @@ -133,12 +133,13 @@ line-ending = "auto"
"__init__.py" = ["F401"]

[tool.mypy]
plugins = ["pydantic.mypy", {% if cookiecutter.use_postgres -%}"sqlalchemy.ext.mypy.plugin"{% endif %}]warn_return_any = false
warn_return_any = false
namespace_packages = true
strict = true
ignore_missing_imports = true
pretty = true
show_error_codes = true
implicit_reexport = true
disable_error_code = ["prop-decorator", "override", "import-untyped"]
exclude = ["migrations"]
plugins = ["pydantic.mypy", {% if cookiecutter.use_postgres -%}"sqlalchemy.ext.mypy.plugin"{% endif %}]
exclude = ["migrations"]
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ class HTTPBearer(_HTTPBearer):
Returns access token as str.
"""

async def __call__(self, request: Request) -> str | None: # type: ignore
async def __call__(self, request: Request) -> str | None:
"""Return access token."""
try:
obj = await super().__call__(request)
return obj.credentials if obj else None
except HTTPException:
except HTTPException as err:
msg = "Missing token."
raise exceptions.Http401(msg)
raise exceptions.Http401(msg) from err
else:
return obj.credentials if obj else None


auth_scheme = HTTPBearer()
Expand Down
4 changes: 3 additions & 1 deletion fastapi_forge/type_info_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pydantic.dataclasses import dataclass

from fastapi_forge.enums import FieldDataTypeEnum
from fastapi_forge.logger import logger

EnumName = Annotated[str, Field(...)]

Expand Down Expand Up @@ -48,9 +49,10 @@ def __init__(self) -> None:

def register(self, key: T, data_type: TypeInfo) -> None:
if key in self:
raise KeyError(
logger.error(
f"{self.__class__.__name__}: Key '{key}' is already registered."
)
return
self._registry[key] = data_type

def get(self, key: T) -> TypeInfo:
Expand Down
82 changes: 69 additions & 13 deletions frontend/src/components/modal/AddFieldModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<div class="input-group">
<label class="field-label">Type</label>
<select class="field-select" v-model="type">
<select class="field-select" v-model="type" :disabled="isPrimaryKey">
<option disabled value="">-- Select type --</option>
<option value="String">String</option>
<option value="Int">Int</option>
Expand All @@ -32,13 +32,24 @@

<div class="input-group">
<label class="field-label">Default value</label>
<select class="field-select" v-model="defaultValue" v-if="type === 'Enum'">
<select
class="field-select"
v-model="defaultValue"
v-if="type === 'Enum'"
:disabled="isPrimaryKey"
>
<option value="" />
<option v-for="v in selectedEnum?.values" :key="v.name" :value="v.name">
{{ v.name }}
</option>
</select>
<input class="field-input" v-model="defaultValue" type="text" v-else />
<input
class="field-input"
v-model="defaultValue"
type="text"
v-else
:disabled="isPrimaryKey || createdAtTimestamp || updatedAtTimestamp"
/>
</div>
</div>

Expand All @@ -49,14 +60,20 @@
<label><input type="checkbox" v-model="isIndex" /> Index</label>
</div>

<div class="checkbox-group" v-if="type === 'DateTime'">
<label class="field-label">Timestamp</label>
<label><input type="checkbox" v-model="createdAtTimestamp" /> Created</label>
<label><input type="checkbox" v-model="updatedAtTimestamp" /> Updated</label>
</div>

<div class="action-group">
<button class="save-field-btn" @click="saveField">Save</button>
</div>
</main>
</template>

<script setup lang="ts">
import { ref } from "vue"
import { ref, watch } from "vue"
import { useProjectStore } from "@/stores/useProjectStore"
import { useModalStore } from "@/stores/useModalStore"
import type { EnumT, FieldType } from "@/types/types"
Expand All @@ -69,14 +86,60 @@ const projectStore = useProjectStore()
const modalStore = useModalStore()

const selectedEnum = ref<EnumT | undefined>()

const fieldName = ref("")
const type = ref("")
const defaultValue = ref("")
const isPrimaryKey = ref(false)
const isNullable = ref(false)
const isUnique = ref(false)
const isIndex = ref(false)
const metadata = ref(null)
const extraKwargs = ref({})
const createdAtTimestamp = ref(false)
const updatedAtTimestamp = ref(false)

watch(isPrimaryKey, (newVal) => {
if (newVal) {
type.value = "UUID"
defaultValue.value = "uuid.uuid4"
}
})

const resetTimestamp = () => {
if (defaultValue.value === "datetime.now(timezone.utc)") {
defaultValue.value = ""
}
extraKwargs.value = {}
}

watch(createdAtTimestamp, (newVal) => {
if (newVal) {
updatedAtTimestamp.value = false
defaultValue.value = "datetime.now(timezone.utc)"
extraKwargs.value = {}
} else if (!updatedAtTimestamp.value) {
resetTimestamp()
}
})

watch(updatedAtTimestamp, (newVal) => {
if (newVal) {
createdAtTimestamp.value = false
defaultValue.value = "datetime.now(timezone.utc)"
extraKwargs.value = { onupdate: "datetime.now(timezone.utc)" }
} else if (!createdAtTimestamp.value) {
resetTimestamp()
}
})

watch(type, (newVal) => {
if (newVal === "DateTime") return
if (createdAtTimestamp.value || updatedAtTimestamp.value) {
resetTimestamp()
createdAtTimestamp.value = false
updatedAtTimestamp.value = false
}
})

const saveField = () => {
if (!fieldName.value || !type.value) return
Expand All @@ -89,6 +152,7 @@ const saveField = () => {
isUnique: isUnique.value,
isIndex: isIndex.value,
defaultValue: defaultValue.value || undefined,
metadata: metadata.value || undefined,
})
modalStore.close()
}
Expand All @@ -101,46 +165,39 @@ const saveField = () => {
gap: 1rem;
padding: 1rem;
}

.input-container {
display: flex;
flex-direction: column;
gap: 1rem;
}

.input-group {
display: flex;
flex-direction: column;
gap: 0.15rem;
}

.field-label {
font-weight: bold;
margin-bottom: 5px;
}

.field-input,
.field-select {
border: 2px solid black;
border-radius: 6px;
padding: 0.6rem;
background-color: white;
}

.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-weight: bold;
}

.action-group {
gap: 0.5rem;
margin-top: 2rem;
display: flex;
flex-direction: column;
}

.save-field-btn {
width: 100%;
padding: 0.5rem 1rem;
Expand All @@ -152,7 +209,6 @@ const saveField = () => {
transform 0.1s ease-in-out,
box-shadow 0.1s ease-in-out;
}

.save-field-btn:hover {
cursor: pointer;
transform: translate(2px, 2px);
Expand Down
Loading