fastapi-auth-starter 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_auth_starter/__init__.py +7 -0
- fastapi_auth_starter/cli.py +326 -0
- fastapi_auth_starter-0.1.3.data/data/README.md +247 -0
- fastapi_auth_starter-0.1.3.data/data/alembic/README +1 -0
- fastapi_auth_starter-0.1.3.data/data/alembic/env.py +100 -0
- fastapi_auth_starter-0.1.3.data/data/alembic/script.py.mako +28 -0
- fastapi_auth_starter-0.1.3.data/data/alembic/versions/279c472f4fd8_add_user_table.py +42 -0
- fastapi_auth_starter-0.1.3.data/data/alembic/versions/5f062b3648fa_change_user_id_from_uuid_to_string_for_.py +38 -0
- fastapi_auth_starter-0.1.3.data/data/alembic/versions/8d275132562b_create_tasks_table.py +44 -0
- fastapi_auth_starter-0.1.3.data/data/alembic.ini +150 -0
- fastapi_auth_starter-0.1.3.data/data/app/__init__.py +5 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/__init__.py +4 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/__init__.py +4 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/api.py +21 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/__init__.py +4 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/auth.py +513 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/health.py +50 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/task.py +182 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/routes/user.py +144 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/__init__.py +8 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/auth.py +198 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/task.py +61 -0
- fastapi_auth_starter-0.1.3.data/data/app/api/v1/schemas/user.py +96 -0
- fastapi_auth_starter-0.1.3.data/data/app/core/__init__.py +4 -0
- fastapi_auth_starter-0.1.3.data/data/app/core/config.py +107 -0
- fastapi_auth_starter-0.1.3.data/data/app/core/database.py +106 -0
- fastapi_auth_starter-0.1.3.data/data/app/core/dependencies.py +148 -0
- fastapi_auth_starter-0.1.3.data/data/app/core/exceptions.py +7 -0
- fastapi_auth_starter-0.1.3.data/data/app/db/__init__.py +4 -0
- fastapi_auth_starter-0.1.3.data/data/app/main.py +91 -0
- fastapi_auth_starter-0.1.3.data/data/app/models/__init__.py +14 -0
- fastapi_auth_starter-0.1.3.data/data/app/models/task.py +56 -0
- fastapi_auth_starter-0.1.3.data/data/app/models/user.py +45 -0
- fastapi_auth_starter-0.1.3.data/data/app/services/__init__.py +8 -0
- fastapi_auth_starter-0.1.3.data/data/app/services/auth.py +405 -0
- fastapi_auth_starter-0.1.3.data/data/app/services/task.py +165 -0
- fastapi_auth_starter-0.1.3.data/data/app/services/user.py +108 -0
- fastapi_auth_starter-0.1.3.data/data/pyproject.toml +77 -0
- fastapi_auth_starter-0.1.3.data/data/runtime.txt +2 -0
- fastapi_auth_starter-0.1.3.data/data/vercel.json +19 -0
- fastapi_auth_starter-0.1.3.dist-info/METADATA +283 -0
- fastapi_auth_starter-0.1.3.dist-info/RECORD +44 -0
- fastapi_auth_starter-0.1.3.dist-info/WHEEL +4 -0
- fastapi_auth_starter-0.1.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI tool for scaffolding new FastAPI projects from this starter template
|
|
3
|
+
Reference: https://docs.python.org/3/library/argparse.html
|
|
4
|
+
"""
|
|
5
|
+
import argparse
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_template_files() -> list[Path]:
|
|
12
|
+
"""
|
|
13
|
+
Get list of template files to copy when scaffolding a new project.
|
|
14
|
+
|
|
15
|
+
Returns paths relative to the package root that should be included.
|
|
16
|
+
"""
|
|
17
|
+
# Files and directories to include in the template
|
|
18
|
+
# These are relative to the project root
|
|
19
|
+
# Note: fastapi_auth_starter/ is excluded - it's only for the package itself
|
|
20
|
+
template_files = [
|
|
21
|
+
"app",
|
|
22
|
+
"alembic",
|
|
23
|
+
"alembic.ini",
|
|
24
|
+
"pyproject.toml",
|
|
25
|
+
"README.md",
|
|
26
|
+
"runtime.txt",
|
|
27
|
+
"vercel.json",
|
|
28
|
+
".gitignore", # Include .gitignore if it exists
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Convert to Path objects
|
|
32
|
+
return [Path(f) for f in template_files]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def copy_template_files(source_dir: Path, dest_dir: Path, project_name: str) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Copy template files from source to destination, customizing as needed.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
source_dir: Source directory containing template files
|
|
41
|
+
dest_dir: Destination directory for new project
|
|
42
|
+
project_name: Name of the new project (for customization)
|
|
43
|
+
"""
|
|
44
|
+
template_files = get_template_files()
|
|
45
|
+
|
|
46
|
+
# Create destination directory
|
|
47
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
# Copy each file/directory
|
|
50
|
+
for item in template_files:
|
|
51
|
+
source_path = source_dir / item
|
|
52
|
+
dest_path = dest_dir / item
|
|
53
|
+
|
|
54
|
+
if not source_path.exists():
|
|
55
|
+
# Skip silently for optional files like .gitignore
|
|
56
|
+
if item != ".gitignore":
|
|
57
|
+
print(f"Warning: Template file {item} not found, skipping...")
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# Explicitly exclude the package directory
|
|
61
|
+
if item == "fastapi_auth_starter":
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
if source_path.is_dir():
|
|
65
|
+
# Copy directory recursively, excluding __pycache__ and .pyc files
|
|
66
|
+
shutil.copytree(
|
|
67
|
+
source_path,
|
|
68
|
+
dest_path,
|
|
69
|
+
dirs_exist_ok=True,
|
|
70
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo")
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
# Copy file
|
|
74
|
+
shutil.copy2(source_path, dest_path)
|
|
75
|
+
|
|
76
|
+
# Create a clean pyproject.toml for standalone application
|
|
77
|
+
pyproject_path = dest_dir / "pyproject.toml"
|
|
78
|
+
if pyproject_path.exists():
|
|
79
|
+
# Read original to extract dependencies
|
|
80
|
+
original_content = pyproject_path.read_text()
|
|
81
|
+
|
|
82
|
+
# Extract dependencies from original
|
|
83
|
+
import re
|
|
84
|
+
deps_match = re.search(r'dependencies = \[(.*?)\]', original_content, re.DOTALL)
|
|
85
|
+
dev_deps_match = re.search(r'\[project\.optional-dependencies\]\s+dev = \[(.*?)\]', original_content, re.DOTALL)
|
|
86
|
+
|
|
87
|
+
dependencies = deps_match.group(1).strip() if deps_match else ""
|
|
88
|
+
dev_dependencies = dev_deps_match.group(1).strip() if dev_deps_match else ""
|
|
89
|
+
|
|
90
|
+
# Sanitize project name
|
|
91
|
+
sanitized_name = project_name.lower().replace(" ", "_").replace("-", "_")
|
|
92
|
+
|
|
93
|
+
# Create clean pyproject.toml for standalone application
|
|
94
|
+
clean_content = f"""[project]
|
|
95
|
+
name = "{sanitized_name}"
|
|
96
|
+
version = "0.1.0"
|
|
97
|
+
description = "FastAPI application"
|
|
98
|
+
readme = "README.md"
|
|
99
|
+
requires-python = ">=3.12"
|
|
100
|
+
dependencies = [
|
|
101
|
+
{dependencies}
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Development dependencies
|
|
105
|
+
[project.optional-dependencies]
|
|
106
|
+
dev = [
|
|
107
|
+
{dev_dependencies}
|
|
108
|
+
]
|
|
109
|
+
"""
|
|
110
|
+
pyproject_path.write_text(clean_content)
|
|
111
|
+
|
|
112
|
+
print(f"✓ Created new FastAPI project: {dest_dir}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def find_package_root() -> Path:
|
|
116
|
+
"""
|
|
117
|
+
Find the root directory containing template files.
|
|
118
|
+
|
|
119
|
+
When installed as a package, template files are in the package's shared-data.
|
|
120
|
+
In development mode, they're in the project root.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
# First, try development mode (current file's parent's parent)
|
|
124
|
+
package_file = Path(__file__).resolve()
|
|
125
|
+
dev_root = package_file.parent.parent
|
|
126
|
+
|
|
127
|
+
# Check if we're in development mode
|
|
128
|
+
if (dev_root / "app").exists() and (dev_root / "alembic").exists():
|
|
129
|
+
return dev_root
|
|
130
|
+
|
|
131
|
+
# Debug: Print where we're looking
|
|
132
|
+
print(f"Debug: Package file location: {package_file}", file=sys.stderr)
|
|
133
|
+
print(f"Debug: Package directory: {package_file.parent}", file=sys.stderr)
|
|
134
|
+
print(f"Debug: Package parent (site-packages): {package_file.parent.parent}", file=sys.stderr)
|
|
135
|
+
|
|
136
|
+
# If installed as a package, template files are in shared-data
|
|
137
|
+
# Hatchling places shared-data files in the package's installation directory
|
|
138
|
+
# We need to find where the package is installed and look for shared-data
|
|
139
|
+
try:
|
|
140
|
+
import site
|
|
141
|
+
import sysconfig
|
|
142
|
+
|
|
143
|
+
# Get all possible site-packages locations
|
|
144
|
+
site_dirs = site.getsitepackages()
|
|
145
|
+
if hasattr(site, 'getsitepackages'):
|
|
146
|
+
# Also check user site-packages
|
|
147
|
+
try:
|
|
148
|
+
user_site = site.getusersitepackages()
|
|
149
|
+
if user_site:
|
|
150
|
+
site_dirs.append(user_site)
|
|
151
|
+
except AttributeError:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# Also check where this package is actually installed
|
|
155
|
+
# The package is at fastapi_auth_starter/__init__.py, so parent is fastapi_auth_starter/
|
|
156
|
+
package_location = Path(__file__).parent
|
|
157
|
+
package_parent = package_location.parent # This is site-packages or similar
|
|
158
|
+
|
|
159
|
+
print(f"Debug: Checking package_parent: {package_parent}", file=sys.stderr)
|
|
160
|
+
print(f"Debug: app exists? {(package_parent / 'app').exists()}", file=sys.stderr)
|
|
161
|
+
print(f"Debug: alembic exists? {(package_parent / 'alembic').exists()}", file=sys.stderr)
|
|
162
|
+
|
|
163
|
+
# List what's actually in site-packages for debugging
|
|
164
|
+
if package_parent.exists():
|
|
165
|
+
print(f"Debug: Contents of {package_parent}:", file=sys.stderr)
|
|
166
|
+
try:
|
|
167
|
+
for item in sorted(package_parent.iterdir())[:30]: # First 30 items
|
|
168
|
+
item_type = "DIR" if item.is_dir() else "FILE"
|
|
169
|
+
print(f"Debug: {item_type}: {item.name}", file=sys.stderr)
|
|
170
|
+
# If we find app or alembic, note it
|
|
171
|
+
if item.name in ["app", "alembic"]:
|
|
172
|
+
print(f"Debug: *** FOUND {item.name} at {item} ***", file=sys.stderr)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
print(f"Debug: Error listing directory: {e}", file=sys.stderr)
|
|
175
|
+
|
|
176
|
+
# Try to find app/alembic by searching recursively (limited depth)
|
|
177
|
+
if package_parent.exists():
|
|
178
|
+
print(f"Debug: Searching for app/ and alembic/ directories...", file=sys.stderr)
|
|
179
|
+
for item in package_parent.iterdir():
|
|
180
|
+
if item.is_dir() and item.name in ["app", "alembic"]:
|
|
181
|
+
# Found one, check if the other exists nearby
|
|
182
|
+
other_name = "alembic" if item.name == "app" else "app"
|
|
183
|
+
other_path = item.parent / other_name
|
|
184
|
+
if other_path.exists():
|
|
185
|
+
print(f"Debug: *** FOUND BOTH at {item.parent} ***", file=sys.stderr)
|
|
186
|
+
return item.parent
|
|
187
|
+
|
|
188
|
+
# Hatchling shared-data places files at the site-packages level
|
|
189
|
+
# So if package is at site-packages/fastapi_auth_starter/
|
|
190
|
+
# Shared data is at site-packages/app/, site-packages/alembic/, etc.
|
|
191
|
+
if package_parent.exists():
|
|
192
|
+
# Check if shared-data is at the site-packages level
|
|
193
|
+
if (package_parent / "app").exists() and (package_parent / "alembic").exists():
|
|
194
|
+
print(f"Debug: Found template files at site-packages level: {package_parent}", file=sys.stderr)
|
|
195
|
+
return package_parent
|
|
196
|
+
|
|
197
|
+
# Also check parent of site-packages (unlikely but possible)
|
|
198
|
+
if package_parent.parent.exists():
|
|
199
|
+
if (package_parent.parent / "app").exists() and (package_parent.parent / "alembic").exists():
|
|
200
|
+
print(f"Debug: Found template files at parent level: {package_parent.parent}", file=sys.stderr)
|
|
201
|
+
return package_parent.parent
|
|
202
|
+
|
|
203
|
+
# Check standard site-packages locations
|
|
204
|
+
for site_dir in site_dirs:
|
|
205
|
+
site_path = Path(site_dir)
|
|
206
|
+
|
|
207
|
+
# Check if shared-data is in site-packages root
|
|
208
|
+
if (site_path / "app").exists() and (site_path / "alembic").exists():
|
|
209
|
+
return site_path
|
|
210
|
+
|
|
211
|
+
# Check in package directory
|
|
212
|
+
package_dir = site_path / "fastapi_auth_starter"
|
|
213
|
+
if package_dir.exists():
|
|
214
|
+
# Check parent (shared-data might be at site-packages level)
|
|
215
|
+
if (site_path / "app").exists():
|
|
216
|
+
return site_path
|
|
217
|
+
# Or in package directory itself
|
|
218
|
+
if (package_dir / "app").exists():
|
|
219
|
+
return package_dir
|
|
220
|
+
|
|
221
|
+
# Check parent of package (hatchling shared-data location)
|
|
222
|
+
package_parent = package_dir.parent
|
|
223
|
+
if (package_parent / "app").exists():
|
|
224
|
+
return package_parent
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
# Log error but continue to fallback
|
|
228
|
+
print(f"Warning: Error checking site-packages: {e}", file=sys.stderr)
|
|
229
|
+
|
|
230
|
+
# Fallback: use development root
|
|
231
|
+
# This handles editable installs and other scenarios
|
|
232
|
+
return dev_root
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print(f"Error finding package root: {e}", file=sys.stderr)
|
|
236
|
+
# Final fallback: use current file's parent's parent
|
|
237
|
+
return Path(__file__).resolve().parent.parent
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def init_project(project_name: str, target_dir: Path | None = None) -> None:
|
|
241
|
+
"""
|
|
242
|
+
Initialize a new FastAPI project from this starter template.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
project_name: Name of the new project
|
|
246
|
+
target_dir: Target directory (defaults to current directory/project_name)
|
|
247
|
+
"""
|
|
248
|
+
if target_dir is None:
|
|
249
|
+
target_dir = Path.cwd() / project_name
|
|
250
|
+
|
|
251
|
+
# Check if target directory already exists
|
|
252
|
+
if target_dir.exists():
|
|
253
|
+
print(f"Error: Directory {target_dir} already exists!")
|
|
254
|
+
sys.exit(1)
|
|
255
|
+
|
|
256
|
+
# Find the template source directory
|
|
257
|
+
source_dir = find_package_root()
|
|
258
|
+
|
|
259
|
+
if not source_dir.exists():
|
|
260
|
+
print(f"Error: Could not find template source directory!")
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
|
|
263
|
+
print(f"Creating new FastAPI project '{project_name}' from template...")
|
|
264
|
+
print(f"Source: {source_dir}")
|
|
265
|
+
print(f"Destination: {target_dir}")
|
|
266
|
+
|
|
267
|
+
# Debug: Verify source directory has required files
|
|
268
|
+
if not (source_dir / "app").exists():
|
|
269
|
+
print(f"Warning: 'app' directory not found at {source_dir / 'app'}", file=sys.stderr)
|
|
270
|
+
if not (source_dir / "alembic").exists():
|
|
271
|
+
print(f"Warning: 'alembic' directory not found at {source_dir / 'alembic'}", file=sys.stderr)
|
|
272
|
+
|
|
273
|
+
# Copy template files
|
|
274
|
+
copy_template_files(source_dir, target_dir, project_name)
|
|
275
|
+
|
|
276
|
+
print("\n✓ Project created successfully!")
|
|
277
|
+
print(f"\nNext steps:")
|
|
278
|
+
print(f" 1. cd {target_dir}")
|
|
279
|
+
print(f" 2. Create a .env file with your configuration")
|
|
280
|
+
print(f" 3. Run: uv sync")
|
|
281
|
+
print(f" 4. Run: uv run alembic upgrade head")
|
|
282
|
+
print(f" 5. Run: uv run uvicorn app.main:app --reload")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def main() -> None:
|
|
286
|
+
"""
|
|
287
|
+
Main CLI entry point.
|
|
288
|
+
Handles command-line arguments and dispatches to appropriate functions.
|
|
289
|
+
"""
|
|
290
|
+
parser = argparse.ArgumentParser(
|
|
291
|
+
description="FastAPI Auth Starter - Scaffold new FastAPI projects",
|
|
292
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
293
|
+
epilog="""
|
|
294
|
+
Examples:
|
|
295
|
+
fastapi-auth-starter init my-api
|
|
296
|
+
fastapi-auth-starter init my-api --dir /path/to/projects
|
|
297
|
+
"""
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
301
|
+
|
|
302
|
+
# Init command
|
|
303
|
+
init_parser = subparsers.add_parser("init", help="Initialize a new FastAPI project")
|
|
304
|
+
init_parser.add_argument(
|
|
305
|
+
"project_name",
|
|
306
|
+
help="Name of the new project"
|
|
307
|
+
)
|
|
308
|
+
init_parser.add_argument(
|
|
309
|
+
"--dir",
|
|
310
|
+
type=Path,
|
|
311
|
+
help="Target directory (defaults to current directory/project_name)"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
args = parser.parse_args()
|
|
315
|
+
|
|
316
|
+
if args.command == "init":
|
|
317
|
+
target_dir = args.dir / args.project_name if args.dir else None
|
|
318
|
+
init_project(args.project_name, target_dir)
|
|
319
|
+
else:
|
|
320
|
+
parser.print_help()
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
if __name__ == "__main__":
|
|
325
|
+
main()
|
|
326
|
+
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# FastAPI Auth Starter
|
|
2
|
+
|
|
3
|
+
A clean architecture FastAPI project starter with PostgreSQL and Alembic migrations.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
fastapi_auth_starter/
|
|
9
|
+
├── app/
|
|
10
|
+
│ ├── __init__.py
|
|
11
|
+
│ ├── main.py # FastAPI application entry point
|
|
12
|
+
│ ├── api/
|
|
13
|
+
│ │ └── v1/
|
|
14
|
+
│ │ ├── api.py # API router aggregation
|
|
15
|
+
│ │ └── routes/
|
|
16
|
+
│ │ └── health.py # Health check endpoint
|
|
17
|
+
│ ├── core/
|
|
18
|
+
│ │ ├── config.py # Application configuration
|
|
19
|
+
│ │ └── database.py # Database connection and session management
|
|
20
|
+
│ ├── models/ # SQLAlchemy database models
|
|
21
|
+
│ ├── services/ # Business logic services
|
|
22
|
+
│ └── db/ # Database utilities
|
|
23
|
+
├── alembic/ # Database migration scripts
|
|
24
|
+
├── alembic.ini # Alembic configuration
|
|
25
|
+
├── pyproject.toml # Project dependencies (uv)
|
|
26
|
+
└── README.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- ✅ Clean architecture with separation of concerns
|
|
32
|
+
- ✅ FastAPI with async SQLAlchemy
|
|
33
|
+
- ✅ PostgreSQL database support
|
|
34
|
+
- ✅ Alembic for database migrations
|
|
35
|
+
- ✅ Health check endpoint
|
|
36
|
+
- ✅ Dependency injection with FastAPI
|
|
37
|
+
- ✅ Environment-based configuration
|
|
38
|
+
|
|
39
|
+
## Prerequisites
|
|
40
|
+
|
|
41
|
+
- Python 3.12+ (3.13 for local dev, 3.12 for Vercel deployment)
|
|
42
|
+
- [uv](https://github.com/astral-sh/uv) package manager
|
|
43
|
+
- PostgreSQL database (local or remote)
|
|
44
|
+
|
|
45
|
+
## Setup
|
|
46
|
+
|
|
47
|
+
### 1. Install Dependencies
|
|
48
|
+
|
|
49
|
+
Dependencies are managed with `uv`. They are automatically installed when you run commands with `uv run`.
|
|
50
|
+
|
|
51
|
+
### 2. Configure Environment Variables
|
|
52
|
+
|
|
53
|
+
**Important:** Copy the example environment file and configure your settings:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cp .env.example .env
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then edit `.env` with your configuration:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Database Configuration
|
|
63
|
+
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/fastapi_auth
|
|
64
|
+
|
|
65
|
+
# API Configuration (optional - defaults are fine)
|
|
66
|
+
API_V1_PREFIX=/api/v1
|
|
67
|
+
PROJECT_NAME=FastAPI Auth Starter
|
|
68
|
+
VERSION=0.1.0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Note:**
|
|
72
|
+
- The `.env` file is gitignored and should not be committed
|
|
73
|
+
- The application uses `asyncpg` for async operations, but Alembic uses `psycopg2` for migrations (sync driver)
|
|
74
|
+
- All sensitive configuration should be in `.env` file, not hardcoded
|
|
75
|
+
|
|
76
|
+
### 3. Initialize Database
|
|
77
|
+
|
|
78
|
+
First, ensure PostgreSQL is running and create the database:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
createdb fastapi_auth
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or using PostgreSQL client:
|
|
85
|
+
|
|
86
|
+
```sql
|
|
87
|
+
CREATE DATABASE fastapi_auth;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 4. Run Migrations
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Create initial migration (if needed)
|
|
94
|
+
uv run alembic revision --autogenerate -m "Initial migration"
|
|
95
|
+
|
|
96
|
+
# Apply migrations
|
|
97
|
+
uv run alembic upgrade head
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Running the Application
|
|
101
|
+
|
|
102
|
+
### Development Server
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
uv run uvicorn app.main:app --reload
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The API will be available at:
|
|
109
|
+
- API: http://localhost:8000
|
|
110
|
+
- Docs: http://localhost:8000/docs
|
|
111
|
+
- ReDoc: http://localhost:8000/redoc
|
|
112
|
+
|
|
113
|
+
### Health Check
|
|
114
|
+
|
|
115
|
+
Test the health endpoint:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
curl http://localhost:8000/api/v1/health
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Expected response:
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"status": "healthy",
|
|
125
|
+
"message": "Service is running"
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
### Adding New Routes
|
|
132
|
+
|
|
133
|
+
1. Create a new route file in `app/api/v1/routes/`
|
|
134
|
+
2. Import and include the router in `app/api/v1/api.py`
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
```python
|
|
138
|
+
# app/api/v1/routes/users.py
|
|
139
|
+
from fastapi import APIRouter
|
|
140
|
+
|
|
141
|
+
router = APIRouter(prefix="/users", tags=["users"])
|
|
142
|
+
|
|
143
|
+
@router.get("/")
|
|
144
|
+
async def get_users():
|
|
145
|
+
return {"users": []}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then add to `app/api/v1/api.py`:
|
|
149
|
+
```python
|
|
150
|
+
from app.api.v1.routes import users
|
|
151
|
+
api_router.include_router(users.router)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Adding Database Models
|
|
155
|
+
|
|
156
|
+
1. Create model in `app/models/`
|
|
157
|
+
2. Import in `app/models/__init__.py`
|
|
158
|
+
3. Import in `alembic/env.py` (for autogenerate)
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
```python
|
|
162
|
+
# app/models/user.py
|
|
163
|
+
from sqlalchemy import Column, Integer, String
|
|
164
|
+
from app.core.database import Base
|
|
165
|
+
|
|
166
|
+
class User(Base):
|
|
167
|
+
__tablename__ = "users"
|
|
168
|
+
|
|
169
|
+
id = Column(Integer, primary_key=True)
|
|
170
|
+
email = Column(String, unique=True, index=True)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Creating Migrations
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Auto-generate migration from model changes
|
|
177
|
+
uv run alembic revision --autogenerate -m "Description of changes"
|
|
178
|
+
|
|
179
|
+
# Create empty migration
|
|
180
|
+
uv run alembic revision -m "Description of changes"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Applying Migrations
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Apply all pending migrations
|
|
187
|
+
uv run alembic upgrade head
|
|
188
|
+
|
|
189
|
+
# Rollback one migration
|
|
190
|
+
uv run alembic downgrade -1
|
|
191
|
+
|
|
192
|
+
# Rollback to specific revision
|
|
193
|
+
uv run alembic downgrade <revision>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Architecture
|
|
197
|
+
|
|
198
|
+
### Layers
|
|
199
|
+
|
|
200
|
+
- **API Layer** (`app/api/`): Route handlers, request/response models
|
|
201
|
+
- **Service Layer** (`app/services/`): Business logic, domain operations
|
|
202
|
+
- **Model Layer** (`app/models/`): SQLAlchemy database models
|
|
203
|
+
- **Core Layer** (`app/core/`): Configuration, database setup, utilities
|
|
204
|
+
|
|
205
|
+
### Dependency Injection
|
|
206
|
+
|
|
207
|
+
FastAPI's dependency injection is used throughout:
|
|
208
|
+
- Database sessions via `get_db()` dependency
|
|
209
|
+
- Configuration via `settings` object
|
|
210
|
+
- Custom dependencies in `app/core/dependencies.py` (create as needed)
|
|
211
|
+
|
|
212
|
+
## Deployment
|
|
213
|
+
|
|
214
|
+
### Vercel Deployment
|
|
215
|
+
|
|
216
|
+
1. **Install Dev Dependencies Locally:**
|
|
217
|
+
```bash
|
|
218
|
+
uv sync # Installs all dependencies including dev
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
2. **Set Environment Variables in Vercel:**
|
|
222
|
+
- Go to your project settings in Vercel
|
|
223
|
+
- Add `DATABASE_URL` environment variable with your PostgreSQL connection string
|
|
224
|
+
- Format: `postgresql+asyncpg://user:password@host:port/database`
|
|
225
|
+
|
|
226
|
+
3. **Deploy:**
|
|
227
|
+
```bash
|
|
228
|
+
vercel --prod
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Note:**
|
|
232
|
+
- Runtime dependencies don't include `psycopg2-binary` or `alembic` (only needed for local migrations)
|
|
233
|
+
- Python 3.12 is used (Vercel doesn't support 3.13 yet)
|
|
234
|
+
- Make sure to run migrations on your database before deploying
|
|
235
|
+
|
|
236
|
+
## References
|
|
237
|
+
|
|
238
|
+
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
|
|
239
|
+
- [SQLAlchemy Async Documentation](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html)
|
|
240
|
+
- [Alembic Documentation](https://alembic.sqlalchemy.org/)
|
|
241
|
+
- [uv Documentation](https://github.com/astral-sh/uv)
|
|
242
|
+
- [Vercel Python Documentation](https://vercel.com/docs/functions/serverless-functions/runtimes/python)
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT
|
|
247
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Generic single-database configuration.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Alembic environment configuration
|
|
3
|
+
Handles database migrations with SQLAlchemy
|
|
4
|
+
Reference: https://alembic.sqlalchemy.org/en/latest/tutorial.html
|
|
5
|
+
"""
|
|
6
|
+
from logging.config import fileConfig
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import engine_from_config, pool
|
|
9
|
+
|
|
10
|
+
from alembic import context
|
|
11
|
+
|
|
12
|
+
# Import application settings and Base
|
|
13
|
+
from app.core.config import settings
|
|
14
|
+
from app.core.database import Base
|
|
15
|
+
|
|
16
|
+
# Import all models here so Alembic can detect them for autogenerate
|
|
17
|
+
# As models are added, import them here
|
|
18
|
+
from app.models import Task, User
|
|
19
|
+
# This is the Alembic Config object, which provides
|
|
20
|
+
# access to the values within the .ini file in use.
|
|
21
|
+
config = context.config
|
|
22
|
+
|
|
23
|
+
# Interpret the config file for Python logging.
|
|
24
|
+
# This line sets up loggers basically.
|
|
25
|
+
if config.config_file_name is not None:
|
|
26
|
+
fileConfig(config.config_file_name)
|
|
27
|
+
|
|
28
|
+
# Override sqlalchemy.url from settings if not set in alembic.ini
|
|
29
|
+
# Convert asyncpg URL to psycopg2 URL for Alembic (Alembic uses sync drivers)
|
|
30
|
+
# Reference: https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
|
|
31
|
+
if not config.get_main_option("sqlalchemy.url"):
|
|
32
|
+
# Convert asyncpg URL to psycopg2 URL (sync driver for migrations)
|
|
33
|
+
db_url = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql+psycopg2://")
|
|
34
|
+
# Escape % for ConfigParser (double % to prevent interpolation)
|
|
35
|
+
db_url = db_url.replace("%", "%%")
|
|
36
|
+
config.set_main_option("sqlalchemy.url", db_url)
|
|
37
|
+
|
|
38
|
+
# Set target_metadata for autogenerate support
|
|
39
|
+
# This tells Alembic what models exist in the application
|
|
40
|
+
target_metadata = Base.metadata
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_migrations_offline() -> None:
|
|
44
|
+
"""
|
|
45
|
+
Run migrations in 'offline' mode.
|
|
46
|
+
|
|
47
|
+
This configures the context with just a URL
|
|
48
|
+
and not an Engine, though an Engine is acceptable
|
|
49
|
+
here as well. By skipping the Engine creation
|
|
50
|
+
we don't even need a DBAPI to be available.
|
|
51
|
+
|
|
52
|
+
Calls to context.execute() here emit the given string to the
|
|
53
|
+
script output.
|
|
54
|
+
"""
|
|
55
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
56
|
+
context.configure(
|
|
57
|
+
url=url,
|
|
58
|
+
target_metadata=target_metadata,
|
|
59
|
+
literal_binds=True,
|
|
60
|
+
dialect_opts={"paramstyle": "named"},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
with context.begin_transaction():
|
|
64
|
+
context.run_migrations()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def run_migrations_online() -> None:
|
|
68
|
+
"""
|
|
69
|
+
Run migrations in 'online' mode.
|
|
70
|
+
|
|
71
|
+
In this scenario we need to create an Engine
|
|
72
|
+
and associate a connection with the context.
|
|
73
|
+
|
|
74
|
+
Reference: https://alembic.sqlalchemy.org/en/latest/tutorial.html#run-a-migration
|
|
75
|
+
"""
|
|
76
|
+
# Create sync engine from configuration
|
|
77
|
+
# Alembic uses sync SQLAlchemy operations
|
|
78
|
+
# We use psycopg2 (sync) for migrations, asyncpg (async) for the app
|
|
79
|
+
connectable = engine_from_config(
|
|
80
|
+
config.get_section(config.config_ini_section, {}),
|
|
81
|
+
prefix="sqlalchemy.",
|
|
82
|
+
poolclass=pool.NullPool, # Don't pool connections for migrations
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
with connectable.connect() as connection:
|
|
86
|
+
context.configure(
|
|
87
|
+
connection=connection,
|
|
88
|
+
target_metadata=target_metadata,
|
|
89
|
+
compare_type=True, # Compare column types when autogenerating
|
|
90
|
+
compare_server_default=True, # Compare server defaults
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
with context.begin_transaction():
|
|
94
|
+
context.run_migrations()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if context.is_offline_mode():
|
|
98
|
+
run_migrations_offline()
|
|
99
|
+
else:
|
|
100
|
+
run_migrations_online()
|