scaffold-api.sh 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env bash
  2. # Generate FastAPI endpoint boilerplate (Pydantic models + CRUD router) to stdout.
  3. #
  4. # Usage: scaffold-api.sh <resource_name>
  5. # Input: one positional arg — a singular resource name (e.g. "user", "order")
  6. # Output: a complete FastAPI module (models + APIRouter CRUD endpoints) on stdout;
  7. # redirect into a file to use it (the script emits to stdout and never
  8. # opens a file itself, so it cannot clobber a user's project)
  9. # Stderr: usage and error messages only
  10. # Exit: 0 ok, 2 usage (missing resource / unknown flag)
  11. #
  12. # Examples:
  13. # scaffold-api.sh user > routers/user.py
  14. # scaffold-api.sh order | tee routers/orders.py
  15. set -uo pipefail
  16. usage() {
  17. cat <<'EOF'
  18. Usage: scaffold-api.sh <resource_name>
  19. Generate FastAPI endpoint boilerplate — Pydantic Create/Update/Response models
  20. plus an APIRouter with list/create/get/update/delete CRUD endpoints — printed to
  21. stdout. Redirect into a module file to use it.
  22. Arguments:
  23. resource_name a singular resource name, e.g. "user" or "order"
  24. Exit codes: 0 ok, 2 usage (missing resource / unknown flag).
  25. Examples:
  26. scaffold-api.sh user > routers/user.py
  27. scaffold-api.sh order | tee routers/orders.py
  28. EOF
  29. }
  30. # Flags first: --help short-circuits; any unknown flag is a hard usage error.
  31. while [[ $# -gt 0 ]]; do
  32. case "$1" in
  33. -h|--help) usage; exit 0 ;;
  34. --) shift; break ;;
  35. -*) echo "scaffold-api.sh: unknown option: $1" >&2; usage >&2; exit 2 ;;
  36. *) break ;;
  37. esac
  38. done
  39. RESOURCE="${1:-}"
  40. if [[ -z "$RESOURCE" ]]; then
  41. echo "scaffold-api.sh: resource name required" >&2
  42. usage >&2
  43. exit 2
  44. fi
  45. # Convert to different cases
  46. RESOURCE_LOWER=$(echo "$RESOURCE" | tr '[:upper:]' '[:lower:]')
  47. RESOURCE_UPPER=$(echo "$RESOURCE" | tr '[:lower:]' '[:upper:]')
  48. RESOURCE_TITLE=$(echo "$RESOURCE_LOWER" | sed 's/\b\(.\)/\u\1/g')
  49. RESOURCE_PLURAL="${RESOURCE_LOWER}s"
  50. cat << EOF
  51. # =============================================================================
  52. # ${RESOURCE_TITLE} Models
  53. # =============================================================================
  54. from pydantic import BaseModel, Field
  55. from datetime import datetime
  56. class ${RESOURCE_TITLE}Create(BaseModel):
  57. """Create ${RESOURCE_LOWER} request."""
  58. name: str = Field(..., min_length=1, max_length=100)
  59. # Add more fields
  60. class ${RESOURCE_TITLE}Update(BaseModel):
  61. """Update ${RESOURCE_LOWER} request (partial)."""
  62. name: str | None = None
  63. # Add more fields
  64. class ${RESOURCE_TITLE}Response(BaseModel):
  65. """${RESOURCE_TITLE} response."""
  66. id: int
  67. name: str
  68. created_at: datetime
  69. updated_at: datetime
  70. model_config = {"from_attributes": True}
  71. # =============================================================================
  72. # ${RESOURCE_TITLE} Router
  73. # =============================================================================
  74. from fastapi import APIRouter, Depends, HTTPException
  75. from typing import Annotated
  76. router = APIRouter(prefix="/${RESOURCE_PLURAL}", tags=["${RESOURCE_PLURAL}"])
  77. @router.get("/", response_model=list[${RESOURCE_TITLE}Response])
  78. async def list_${RESOURCE_PLURAL}(
  79. db: DB,
  80. skip: int = 0,
  81. limit: int = 10,
  82. ):
  83. """List all ${RESOURCE_PLURAL}."""
  84. result = await db.execute(
  85. select(${RESOURCE_TITLE}).offset(skip).limit(limit)
  86. )
  87. return result.scalars().all()
  88. @router.post("/", response_model=${RESOURCE_TITLE}Response, status_code=201)
  89. async def create_${RESOURCE_LOWER}(data: ${RESOURCE_TITLE}Create, db: DB):
  90. """Create a new ${RESOURCE_LOWER}."""
  91. ${RESOURCE_LOWER} = ${RESOURCE_TITLE}(**data.model_dump())
  92. db.add(${RESOURCE_LOWER})
  93. await db.commit()
  94. await db.refresh(${RESOURCE_LOWER})
  95. return ${RESOURCE_LOWER}
  96. @router.get("/{${RESOURCE_LOWER}_id}", response_model=${RESOURCE_TITLE}Response)
  97. async def get_${RESOURCE_LOWER}(${RESOURCE_LOWER}_id: int, db: DB):
  98. """Get a ${RESOURCE_LOWER} by ID."""
  99. ${RESOURCE_LOWER} = await db.get(${RESOURCE_TITLE}, ${RESOURCE_LOWER}_id)
  100. if not ${RESOURCE_LOWER}:
  101. raise HTTPException(status_code=404, detail="${RESOURCE_TITLE} not found")
  102. return ${RESOURCE_LOWER}
  103. @router.patch("/{${RESOURCE_LOWER}_id}", response_model=${RESOURCE_TITLE}Response)
  104. async def update_${RESOURCE_LOWER}(
  105. ${RESOURCE_LOWER}_id: int,
  106. data: ${RESOURCE_TITLE}Update,
  107. db: DB,
  108. ):
  109. """Update a ${RESOURCE_LOWER}."""
  110. ${RESOURCE_LOWER} = await db.get(${RESOURCE_TITLE}, ${RESOURCE_LOWER}_id)
  111. if not ${RESOURCE_LOWER}:
  112. raise HTTPException(status_code=404, detail="${RESOURCE_TITLE} not found")
  113. for field, value in data.model_dump(exclude_unset=True).items():
  114. setattr(${RESOURCE_LOWER}, field, value)
  115. await db.commit()
  116. await db.refresh(${RESOURCE_LOWER})
  117. return ${RESOURCE_LOWER}
  118. @router.delete("/{${RESOURCE_LOWER}_id}", status_code=204)
  119. async def delete_${RESOURCE_LOWER}(${RESOURCE_LOWER}_id: int, db: DB):
  120. """Delete a ${RESOURCE_LOWER}."""
  121. ${RESOURCE_LOWER} = await db.get(${RESOURCE_TITLE}, ${RESOURCE_LOWER}_id)
  122. if not ${RESOURCE_LOWER}:
  123. raise HTTPException(status_code=404, detail="${RESOURCE_TITLE} not found")
  124. await db.delete(${RESOURCE_LOWER})
  125. await db.commit()
  126. # =============================================================================
  127. # Include in main app:
  128. # from routers.${RESOURCE_PLURAL} import router as ${RESOURCE_PLURAL}_router
  129. # app.include_router(${RESOURCE_PLURAL}_router, prefix="/api/v1")
  130. # =============================================================================
  131. EOF