gentem 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.
- gentem/__init__.py +3 -0
- gentem/__main__.py +6 -0
- gentem/cli.py +163 -0
- gentem/commands/__init__.py +1 -0
- gentem/commands/fastapi.py +746 -0
- gentem/commands/new.py +741 -0
- gentem/template_engine.py +191 -0
- gentem/utils/__init__.py +13 -0
- gentem/utils/validators.py +158 -0
- gentem-0.1.3.dist-info/METADATA +183 -0
- gentem-0.1.3.dist-info/RECORD +15 -0
- gentem-0.1.3.dist-info/WHEEL +5 -0
- gentem-0.1.3.dist-info/entry_points.txt +2 -0
- gentem-0.1.3.dist-info/licenses/LICENSE +21 -0
- gentem-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
"""Implementation of the `gentem fastapi` command."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from rich import print
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from gentem.utils.validators import (
|
|
11
|
+
ValidationError,
|
|
12
|
+
validate_db_type,
|
|
13
|
+
validate_project_name,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_slug(name: str) -> str:
|
|
18
|
+
"""Generate a URL-safe slug from the project name."""
|
|
19
|
+
return name.lower().replace("_", "-")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_class_name(name: str) -> str:
|
|
23
|
+
"""Generate a Python class name from the project name."""
|
|
24
|
+
return "".join(word.capitalize() for word in name.split("_"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_context(
|
|
28
|
+
project_name: str,
|
|
29
|
+
async_mode: bool,
|
|
30
|
+
db_type: Optional[str],
|
|
31
|
+
author: str,
|
|
32
|
+
description: str,
|
|
33
|
+
) -> dict:
|
|
34
|
+
"""Generate the template context for a FastAPI project.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
project_name: Name of the project.
|
|
38
|
+
async_mode: Whether to use async mode.
|
|
39
|
+
db_type: Database type (asyncpg, etc.).
|
|
40
|
+
author: Author name.
|
|
41
|
+
description: Project description.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary of template variables.
|
|
45
|
+
"""
|
|
46
|
+
now = datetime.now()
|
|
47
|
+
slug = generate_slug(project_name)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"project_name": project_name,
|
|
51
|
+
"project_slug": slug,
|
|
52
|
+
"class_name": generate_class_name(project_name),
|
|
53
|
+
"author": author or "Gentem User",
|
|
54
|
+
"email": f"user@{slug}.dev",
|
|
55
|
+
"description": description or f"A FastAPI project generated by Gentem.",
|
|
56
|
+
"version": "0.1.0",
|
|
57
|
+
"python_version": "3.10",
|
|
58
|
+
"python_versions": ["3.10", "3.11", "3.12"],
|
|
59
|
+
"async_mode": async_mode,
|
|
60
|
+
"db_type": db_type,
|
|
61
|
+
"has_database": db_type is not None,
|
|
62
|
+
"year": now.year,
|
|
63
|
+
"month": now.strftime("%B"),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_fastapi_project(
|
|
68
|
+
project_name: str,
|
|
69
|
+
async_mode: bool = False,
|
|
70
|
+
db_type: str = "",
|
|
71
|
+
author: str = "",
|
|
72
|
+
description: str = "",
|
|
73
|
+
dry_run: bool = False,
|
|
74
|
+
verbose: bool = False,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Create a new FastAPI project.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
project_name: Name of the project to create.
|
|
80
|
+
async_mode: Use async mode with lifespan.
|
|
81
|
+
db_type: Database type (asyncpg for async SQLAlchemy).
|
|
82
|
+
author: Author name.
|
|
83
|
+
description: Project description.
|
|
84
|
+
dry_run: Preview without creating files.
|
|
85
|
+
verbose: Show verbose output.
|
|
86
|
+
"""
|
|
87
|
+
# Validate inputs
|
|
88
|
+
try:
|
|
89
|
+
project_name = validate_project_name(project_name)
|
|
90
|
+
db_type = validate_db_type(db_type)
|
|
91
|
+
except ValidationError as e:
|
|
92
|
+
print(f"[red]Error: {e}[/]")
|
|
93
|
+
raise SystemExit(1)
|
|
94
|
+
|
|
95
|
+
# Generate template context
|
|
96
|
+
context = generate_context(
|
|
97
|
+
project_name=project_name,
|
|
98
|
+
async_mode=async_mode,
|
|
99
|
+
db_type=db_type,
|
|
100
|
+
author=author,
|
|
101
|
+
description=description,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Determine output path
|
|
105
|
+
output_path = Path.cwd() / project_name
|
|
106
|
+
|
|
107
|
+
if verbose:
|
|
108
|
+
print(f"Output path: {output_path}")
|
|
109
|
+
print(f"Async mode: {async_mode}")
|
|
110
|
+
print(f"Database type: {db_type or 'none'}")
|
|
111
|
+
|
|
112
|
+
# Show summary
|
|
113
|
+
db_info = f"Database: {db_type}" if db_type else "No database"
|
|
114
|
+
print(Panel(
|
|
115
|
+
f"[bold]Creating new FastAPI project:[/] [cyan]{project_name}[/]\n"
|
|
116
|
+
f"[dim]Async:[/] {async_mode} | [dim]{db_info}[/]\n"
|
|
117
|
+
f"[dim]Author:[/] {context['author']}",
|
|
118
|
+
title="Gentem",
|
|
119
|
+
expand=False,
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
if dry_run:
|
|
123
|
+
print("[yellow]DRY RUN - No files will be created[/]")
|
|
124
|
+
print(f"\nProject would be created at: {output_path}")
|
|
125
|
+
print("\nFiles that would be created:")
|
|
126
|
+
files = get_fastapi_project_files(project_name, db_type is not None)
|
|
127
|
+
for item in files:
|
|
128
|
+
print(f" - {item}")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
# Create project files
|
|
132
|
+
try:
|
|
133
|
+
create_fastapi_project_files(
|
|
134
|
+
project_name=project_name,
|
|
135
|
+
context=context,
|
|
136
|
+
output_path=output_path,
|
|
137
|
+
async_mode=async_mode,
|
|
138
|
+
db_type=db_type,
|
|
139
|
+
)
|
|
140
|
+
print(f"\n[green]✓ FastAPI project '{project_name}' created successfully![/]")
|
|
141
|
+
print(f"\nNext steps:")
|
|
142
|
+
print(f" cd {project_name}")
|
|
143
|
+
print(f" pip install -r requirements.txt")
|
|
144
|
+
print(f" uvicorn {context['project_slug']}.main:app --reload")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print(f"[red]Error creating project: {e}[/]")
|
|
147
|
+
raise SystemExit(1)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_fastapi_project_files(project_name: str, has_database: bool) -> list[str]:
|
|
151
|
+
"""Get list of files that would be created for a FastAPI project.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
project_name: Name of the project.
|
|
155
|
+
has_database: Whether database support is included.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
List of file paths.
|
|
159
|
+
"""
|
|
160
|
+
files = [
|
|
161
|
+
f"{project_name}/",
|
|
162
|
+
f"{project_name}/requirements.txt",
|
|
163
|
+
f"{project_name}/.env",
|
|
164
|
+
f"{project_name}/.gitignore",
|
|
165
|
+
f"{project_name}/README.md",
|
|
166
|
+
f"{project_name}/app/",
|
|
167
|
+
f"{project_name}/app/__init__.py",
|
|
168
|
+
f"{project_name}/app/main.py",
|
|
169
|
+
f"{project_name}/app/core/",
|
|
170
|
+
f"{project_name}/app/core/__init__.py",
|
|
171
|
+
f"{project_name}/app/core/config.py",
|
|
172
|
+
f"{project_name}/app/core/exceptions.py",
|
|
173
|
+
f"{project_name}/app/deps/",
|
|
174
|
+
f"{project_name}/app/deps/__init__.py",
|
|
175
|
+
f"{project_name}/app/utils/",
|
|
176
|
+
f"{project_name}/app/utils/__init__.py",
|
|
177
|
+
f"{project_name}/app/v1/",
|
|
178
|
+
f"{project_name}/app/v1/__init__.py",
|
|
179
|
+
f"{project_name}/app/v1/apis/",
|
|
180
|
+
f"{project_name}/app/v1/apis/__init__.py",
|
|
181
|
+
f"{project_name}/app/v1/apis/routes.py",
|
|
182
|
+
f"{project_name}/app/services/",
|
|
183
|
+
f"{project_name}/app/services/__init__.py",
|
|
184
|
+
f"{project_name}/app/schemas/",
|
|
185
|
+
f"{project_name}/app/schemas/__init__.py",
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
if has_database:
|
|
189
|
+
files.extend([
|
|
190
|
+
f"{project_name}/app/models/",
|
|
191
|
+
f"{project_name}/app/models/__init__.py",
|
|
192
|
+
f"{project_name}/app/core/database.py",
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
return files
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def create_fastapi_project_files(
|
|
199
|
+
project_name: str,
|
|
200
|
+
context: dict,
|
|
201
|
+
output_path: Path,
|
|
202
|
+
async_mode: bool,
|
|
203
|
+
db_type: Optional[str],
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Create all FastAPI project files.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
project_name: Name of the project.
|
|
209
|
+
context: Template context.
|
|
210
|
+
output_path: Path to create the project at.
|
|
211
|
+
async_mode: Whether to use async mode.
|
|
212
|
+
db_type: Database type.
|
|
213
|
+
"""
|
|
214
|
+
slug = context["project_slug"]
|
|
215
|
+
|
|
216
|
+
# Create directories
|
|
217
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
|
|
219
|
+
# Create requirements.txt
|
|
220
|
+
requirements = [
|
|
221
|
+
"fastapi>=0.104.0",
|
|
222
|
+
"uvicorn[standard]>=0.24.0",
|
|
223
|
+
"pydantic>=2.5.0",
|
|
224
|
+
"pydantic-settings>=2.0.0",
|
|
225
|
+
"python-multipart>=0.0.6",
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
if db_type == "asyncpg":
|
|
229
|
+
requirements.extend([
|
|
230
|
+
"sqlalchemy[asyncio]>=2.0.0",
|
|
231
|
+
"asyncpg>=0.29.0",
|
|
232
|
+
])
|
|
233
|
+
|
|
234
|
+
requirements_content = "\n".join(sorted(requirements))
|
|
235
|
+
|
|
236
|
+
# Create .env
|
|
237
|
+
env_content = f"""# {project_name} Environment Variables
|
|
238
|
+
|
|
239
|
+
# Application
|
|
240
|
+
APP_NAME={project_name}
|
|
241
|
+
DEBUG=true
|
|
242
|
+
API_V1_STR=/api/v1
|
|
243
|
+
|
|
244
|
+
# Database
|
|
245
|
+
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/{slug}
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
# Create .gitignore
|
|
249
|
+
gitignore_content = """# Byte-compiled / optimized / DLL files
|
|
250
|
+
__pycache__/
|
|
251
|
+
*.py[cod]
|
|
252
|
+
*$py.class
|
|
253
|
+
|
|
254
|
+
# C extensions
|
|
255
|
+
*.so
|
|
256
|
+
|
|
257
|
+
# Distribution / packaging
|
|
258
|
+
.Python
|
|
259
|
+
build/
|
|
260
|
+
develop-eggs/
|
|
261
|
+
dist/
|
|
262
|
+
downloads/
|
|
263
|
+
eggs/
|
|
264
|
+
.eggs/
|
|
265
|
+
lib/
|
|
266
|
+
lib64/
|
|
267
|
+
parts/
|
|
268
|
+
sdist/
|
|
269
|
+
var/
|
|
270
|
+
wheels/
|
|
271
|
+
*.egg-info/
|
|
272
|
+
.installed.cfg
|
|
273
|
+
*.egg
|
|
274
|
+
|
|
275
|
+
# PyInstaller
|
|
276
|
+
*.manifest
|
|
277
|
+
*.spec
|
|
278
|
+
|
|
279
|
+
# Installer logs
|
|
280
|
+
pip-log.txt
|
|
281
|
+
pip-delete-this-directory.txt
|
|
282
|
+
|
|
283
|
+
# Unit test / coverage reports
|
|
284
|
+
htmlcov/
|
|
285
|
+
.tox/
|
|
286
|
+
.nox/
|
|
287
|
+
.coverage
|
|
288
|
+
.coverage.*
|
|
289
|
+
.cache
|
|
290
|
+
nosetests.xml
|
|
291
|
+
coverage.xml
|
|
292
|
+
*.cover
|
|
293
|
+
*.py,cover
|
|
294
|
+
.hypothesis/
|
|
295
|
+
.pytest_cache/
|
|
296
|
+
|
|
297
|
+
# Jupyter Notebook
|
|
298
|
+
.ipynb_checkpoints
|
|
299
|
+
|
|
300
|
+
# IPython
|
|
301
|
+
profile_default/
|
|
302
|
+
ipython_config.py
|
|
303
|
+
|
|
304
|
+
# pyenv
|
|
305
|
+
.python-version
|
|
306
|
+
|
|
307
|
+
# pipenv
|
|
308
|
+
Pipfile.lock
|
|
309
|
+
|
|
310
|
+
# Environments
|
|
311
|
+
.env
|
|
312
|
+
.venv
|
|
313
|
+
env/
|
|
314
|
+
venv/
|
|
315
|
+
ENV/
|
|
316
|
+
env.bak/
|
|
317
|
+
venv.bak/
|
|
318
|
+
|
|
319
|
+
# IDEs
|
|
320
|
+
.vscode/
|
|
321
|
+
.idea/
|
|
322
|
+
*.swp
|
|
323
|
+
*.swo
|
|
324
|
+
*~
|
|
325
|
+
|
|
326
|
+
# OS
|
|
327
|
+
.DS_Store
|
|
328
|
+
Thumbs.db
|
|
329
|
+
|
|
330
|
+
# Logs
|
|
331
|
+
logs/
|
|
332
|
+
*.log
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
# Create README.md
|
|
336
|
+
async_note = "Async with lifespan" if async_mode else "Sync with lifespan"
|
|
337
|
+
db_note = "Database support with asyncpg + SQLAlchemy" if db_type == "asyncpg" else "No database"
|
|
338
|
+
models_note = "(with --db asyncpg)" if db_type == "asyncpg" else ""
|
|
339
|
+
|
|
340
|
+
readme_content = f"""# {project_name}
|
|
341
|
+
|
|
342
|
+
{context["description"]}
|
|
343
|
+
|
|
344
|
+
## Features
|
|
345
|
+
|
|
346
|
+
- FastAPI REST API
|
|
347
|
+
- {async_note}
|
|
348
|
+
- {db_note}
|
|
349
|
+
- Project structure with core, deps, utils, v1/apis, services, schemas
|
|
350
|
+
- Exception handlers
|
|
351
|
+
- Middleware setup
|
|
352
|
+
- Health check endpoint
|
|
353
|
+
|
|
354
|
+
## Installation
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
pip install -r requirements.txt
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Running
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
uvicorn {slug}.main:app --reload
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## API Documentation
|
|
367
|
+
|
|
368
|
+
Once running, access:
|
|
369
|
+
- Swagger UI: http://localhost:8000/docs
|
|
370
|
+
- ReDoc: http://localhost:8000/redoc
|
|
371
|
+
- OpenAPI: http://localhost:8000/openapi.json
|
|
372
|
+
|
|
373
|
+
## Project Structure
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
app/
|
|
377
|
+
├── __init__.py
|
|
378
|
+
├── main.py # FastAPI application
|
|
379
|
+
├── core/
|
|
380
|
+
│ ├── __init__.py
|
|
381
|
+
│ ├── config.py # Settings
|
|
382
|
+
│ └── exceptions.py # Custom exceptions
|
|
383
|
+
├── deps/
|
|
384
|
+
│ └── __init__.py # Dependencies
|
|
385
|
+
├── utils/
|
|
386
|
+
│ └── __init__.py # Utility functions
|
|
387
|
+
├── v1/
|
|
388
|
+
│ ├── __init__.py
|
|
389
|
+
│ └── apis/
|
|
390
|
+
│ ├── __init__.py
|
|
391
|
+
│ └── routes.py # API routes
|
|
392
|
+
├── services/
|
|
393
|
+
│ └── __init__.py # Business logic
|
|
394
|
+
├── schemas/
|
|
395
|
+
│ └── __init__.py # Pydantic schemas
|
|
396
|
+
└── models/
|
|
397
|
+
└── __init__.py # SQLAlchemy models {models_note}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## License
|
|
401
|
+
|
|
402
|
+
This project is licensed under the MIT License.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
# Write common files
|
|
406
|
+
(output_path / "requirements.txt").write_text(requirements_content, encoding="utf-8")
|
|
407
|
+
(output_path / ".env").write_text(env_content, encoding="utf-8")
|
|
408
|
+
(output_path / ".gitignore").write_text(gitignore_content, encoding="utf-8")
|
|
409
|
+
(output_path / "README.md").write_text(readme_content, encoding="utf-8")
|
|
410
|
+
|
|
411
|
+
# Create app directory structure
|
|
412
|
+
app_dir = output_path / "app"
|
|
413
|
+
app_dir.mkdir(parents=True, exist_ok=True)
|
|
414
|
+
(app_dir / "__init__.py").write_text(f'""" {project_name} - FastAPI application."""\n', encoding="utf-8")
|
|
415
|
+
|
|
416
|
+
# Create core module
|
|
417
|
+
core_dir = app_dir / "core"
|
|
418
|
+
core_dir.mkdir(parents=True, exist_ok=True)
|
|
419
|
+
(core_dir / "__init__.py").write_text('"""Core module: config, exceptions, etc."""\n', encoding="utf-8")
|
|
420
|
+
|
|
421
|
+
# Create config.py
|
|
422
|
+
config_py = f'''"""Application configuration."""
|
|
423
|
+
|
|
424
|
+
from pydantic import Field
|
|
425
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class Settings(BaseSettings):
|
|
429
|
+
"""Application settings."""
|
|
430
|
+
|
|
431
|
+
model_config = SettingsConfigDict(
|
|
432
|
+
env_file=".env",
|
|
433
|
+
env_file_encoding="utf-8",
|
|
434
|
+
extra="ignore",
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
app_name: str = Field(default="{project_name}", description="Application name")
|
|
438
|
+
debug: bool = Field(default=False, description="Debug mode")
|
|
439
|
+
api_v1_str: str = Field(default="/api/v1", description="API version 1 prefix")
|
|
440
|
+
database_url: str = Field(default="", description="Database connection URL")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
settings = Settings()
|
|
444
|
+
'''
|
|
445
|
+
(core_dir / "config.py").write_text(config_py, encoding="utf-8")
|
|
446
|
+
|
|
447
|
+
# Create exceptions.py
|
|
448
|
+
exceptions_py = '''"""Custom exceptions and exception handlers."""
|
|
449
|
+
|
|
450
|
+
from fastapi import HTTPException, Request
|
|
451
|
+
from fastapi.responses import JSONResponse
|
|
452
|
+
from typing import Any, Dict
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class AppException(Exception):
|
|
456
|
+
"""Base application exception."""
|
|
457
|
+
|
|
458
|
+
def __init__(self, message: str, status_code: int = 500, details: Dict[str, Any] = None):
|
|
459
|
+
self.message = message
|
|
460
|
+
self.status_code = status_code
|
|
461
|
+
self.details = details or {}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class NotFoundException(AppException):
|
|
465
|
+
"""Resource not found."""
|
|
466
|
+
|
|
467
|
+
def __init__(self, resource: str = "Resource"):
|
|
468
|
+
super().__init__(
|
|
469
|
+
message=f"{resource} not found",
|
|
470
|
+
status_code=404,
|
|
471
|
+
details={"resource": resource},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class ValidationException(AppException):
|
|
476
|
+
"""Validation error."""
|
|
477
|
+
|
|
478
|
+
def __init__(self, message: str, details: Dict[str, Any] = None):
|
|
479
|
+
super().__init__(
|
|
480
|
+
message=message,
|
|
481
|
+
status_code=422,
|
|
482
|
+
details=details,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
async def app_exception_handler(request: Request, exc: AppException) -> JSONResponse:
|
|
487
|
+
"""Handle AppException."""
|
|
488
|
+
return JSONResponse(
|
|
489
|
+
status_code=exc.status_code,
|
|
490
|
+
content={
|
|
491
|
+
"error": {
|
|
492
|
+
"message": exc.message,
|
|
493
|
+
"details": exc.details,
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
|
500
|
+
"""Handle HTTPException."""
|
|
501
|
+
return JSONResponse(
|
|
502
|
+
status_code=exc.status_code,
|
|
503
|
+
content={
|
|
504
|
+
"error": {
|
|
505
|
+
"message": str(exc.detail) if exc.detail else "HTTP Error",
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
async def validation_exception_handler(request: Request, exc) -> JSONResponse:
|
|
512
|
+
"""Handle validation errors from Pydantic."""
|
|
513
|
+
return JSONResponse(
|
|
514
|
+
status_code=422,
|
|
515
|
+
content={
|
|
516
|
+
"error": {
|
|
517
|
+
"message": "Validation error",
|
|
518
|
+
"details": exc.errors() if hasattr(exc, "errors") else str(exc),
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def setup_exception_handlers(app) -> None:
|
|
525
|
+
"""Setup exception handlers for the FastAPI app."""
|
|
526
|
+
app.add_exception_handler(AppException, app_exception_handler)
|
|
527
|
+
app.add_exception_handler(HTTPException, http_exception_handler)
|
|
528
|
+
app.add_exception_handler(ValidationException, validation_exception_handler)
|
|
529
|
+
'''
|
|
530
|
+
(core_dir / "exceptions.py").write_text(exceptions_py, encoding="utf-8")
|
|
531
|
+
|
|
532
|
+
# Create deps module
|
|
533
|
+
deps_dir = app_dir / "deps"
|
|
534
|
+
deps_dir.mkdir(parents=True, exist_ok=True)
|
|
535
|
+
(deps_dir / "__init__.py").write_text('"""Dependencies."""\n', encoding="utf-8")
|
|
536
|
+
|
|
537
|
+
# Create utils module
|
|
538
|
+
utils_dir = app_dir / "utils"
|
|
539
|
+
utils_dir.mkdir(parents=True, exist_ok=True)
|
|
540
|
+
(utils_dir / "__init__.py").write_text('"""Utility functions."""\n', encoding="utf-8")
|
|
541
|
+
|
|
542
|
+
# Create v1 module
|
|
543
|
+
v1_dir = app_dir / "v1"
|
|
544
|
+
v1_dir.mkdir(parents=True, exist_ok=True)
|
|
545
|
+
(v1_dir / "__init__.py").write_text('"""API version 1."""\n', encoding="utf-8")
|
|
546
|
+
|
|
547
|
+
# Create v1/apis module
|
|
548
|
+
apis_dir = v1_dir / "apis"
|
|
549
|
+
apis_dir.mkdir(parents=True, exist_ok=True)
|
|
550
|
+
(apis_dir / "__init__.py").write_text('"""API routes."""\n', encoding="utf-8")
|
|
551
|
+
|
|
552
|
+
# Create routes.py
|
|
553
|
+
routes_py = f'''"""API routes."""
|
|
554
|
+
|
|
555
|
+
from fastapi import APIRouter
|
|
556
|
+
from typing import Any, Dict
|
|
557
|
+
|
|
558
|
+
router = APIRouter()
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@router.get("/healthz", tags=["Health"])
|
|
562
|
+
async def health_check() -> Dict[str, str]:
|
|
563
|
+
"""Health check endpoint."""
|
|
564
|
+
return {{"status": "healthy"}}
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@router.get("/", tags=["Root"])
|
|
568
|
+
async def root() -> Dict[str, Any]:
|
|
569
|
+
"""Root endpoint."""
|
|
570
|
+
return {{
|
|
571
|
+
"app": "{project_name}",
|
|
572
|
+
"version": "{context["version"]}",
|
|
573
|
+
"docs": "/docs",
|
|
574
|
+
}}
|
|
575
|
+
'''
|
|
576
|
+
(apis_dir / "routes.py").write_text(routes_py, encoding="utf-8")
|
|
577
|
+
|
|
578
|
+
# Create services module
|
|
579
|
+
services_dir = app_dir / "services"
|
|
580
|
+
services_dir.mkdir(parents=True, exist_ok=True)
|
|
581
|
+
(services_dir / "__init__.py").write_text('"""Business logic services."""\n', encoding="utf-8")
|
|
582
|
+
|
|
583
|
+
# Create schemas module
|
|
584
|
+
schemas_dir = app_dir / "schemas"
|
|
585
|
+
schemas_dir.mkdir(parents=True, exist_ok=True)
|
|
586
|
+
(schemas_dir / "__init__.py").write_text('"""Pydantic schemas."""\n', encoding="utf-8")
|
|
587
|
+
|
|
588
|
+
# Create database module if db_type is set
|
|
589
|
+
if db_type == "asyncpg":
|
|
590
|
+
database_py = f'''"""Database configuration and session management."""
|
|
591
|
+
|
|
592
|
+
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
|
593
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
594
|
+
|
|
595
|
+
from app.core.config import settings
|
|
596
|
+
|
|
597
|
+
# Create async engine
|
|
598
|
+
engine = create_async_engine(
|
|
599
|
+
settings.database_url,
|
|
600
|
+
echo=settings.debug,
|
|
601
|
+
pool_pre_ping=True,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Create async session factory
|
|
605
|
+
async_session_factory = async_sessionmaker(
|
|
606
|
+
engine,
|
|
607
|
+
class_=AsyncSession,
|
|
608
|
+
expire_on_commit=False,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class Base(DeclarativeBase):
|
|
613
|
+
"""Base class for SQLAlchemy models."""
|
|
614
|
+
pass
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
async def get_db() -> AsyncSession:
|
|
618
|
+
"""Dependency that provides a database session."""
|
|
619
|
+
async with async_session_factory() as session:
|
|
620
|
+
try:
|
|
621
|
+
yield session
|
|
622
|
+
finally:
|
|
623
|
+
await session.close()
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
async def init_db() -> None:
|
|
627
|
+
"""Initialize database tables."""
|
|
628
|
+
async with engine.begin() as conn:
|
|
629
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
async def close_db() -> None:
|
|
633
|
+
"""Close database connections."""
|
|
634
|
+
await engine.dispose()
|
|
635
|
+
'''
|
|
636
|
+
(core_dir / "database.py").write_text(database_py, encoding="utf-8")
|
|
637
|
+
|
|
638
|
+
# Create models module
|
|
639
|
+
models_dir = app_dir / "models"
|
|
640
|
+
models_dir.mkdir(parents=True, exist_ok=True)
|
|
641
|
+
(models_dir / "__init__.py").write_text('''"""SQLAlchemy models."""
|
|
642
|
+
from app.core.database import Base
|
|
643
|
+
|
|
644
|
+
# Import models here
|
|
645
|
+
# Example:
|
|
646
|
+
# class User(Base):
|
|
647
|
+
# __tablename__ = "users"
|
|
648
|
+
# id = Column(Integer, primary_key=True, index=True)
|
|
649
|
+
# email = Column(String, unique=True, index=True)
|
|
650
|
+
''', encoding="utf-8")
|
|
651
|
+
else:
|
|
652
|
+
# Create empty models __init__.py
|
|
653
|
+
models_dir = app_dir / "models"
|
|
654
|
+
models_dir.mkdir(parents=True, exist_ok=True)
|
|
655
|
+
(models_dir / "__init__.py").write_text('''"""SQLAlchemy models."""
|
|
656
|
+
# Add models here
|
|
657
|
+
# Example:
|
|
658
|
+
# from app.core.database import Base
|
|
659
|
+
# from sqlalchemy import Column, Integer, String
|
|
660
|
+
#
|
|
661
|
+
# class User(Base):
|
|
662
|
+
# __tablename__ = "users"
|
|
663
|
+
# id = Column(Integer, primary_key=True, index=True)
|
|
664
|
+
''', encoding="utf-8")
|
|
665
|
+
|
|
666
|
+
# Create main.py
|
|
667
|
+
if async_mode:
|
|
668
|
+
lifespan_section = '''
|
|
669
|
+
from contextlib import asynccontextmanager
|
|
670
|
+
|
|
671
|
+
from app.core.database import init_db, close_db
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@asynccontextmanager
|
|
675
|
+
async def lifespan(app):
|
|
676
|
+
"""Lifespan context manager for startup and shutdown events."""
|
|
677
|
+
# Startup
|
|
678
|
+
print("Starting up...")
|
|
679
|
+
await init_db()
|
|
680
|
+
yield
|
|
681
|
+
# Shutdown
|
|
682
|
+
print("Shutting down...")
|
|
683
|
+
await close_db()
|
|
684
|
+
'''
|
|
685
|
+
lifespan_arg = "lifespan=lifespan"
|
|
686
|
+
else:
|
|
687
|
+
lifespan_section = '''
|
|
688
|
+
# No lifespan - simple startup
|
|
689
|
+
'''
|
|
690
|
+
lifespan_arg = ""
|
|
691
|
+
|
|
692
|
+
main_py = f'''"""FastAPI application entry point."""
|
|
693
|
+
|
|
694
|
+
from fastapi import FastAPI
|
|
695
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
696
|
+
|
|
697
|
+
from app.core.config import settings
|
|
698
|
+
from app.core.exceptions import setup_exception_handlers
|
|
699
|
+
from app.v1.apis.routes import router as v1_router
|
|
700
|
+
{lifespan_section}
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def create_app() -> FastAPI:
|
|
704
|
+
"""Create and configure the FastAPI application."""
|
|
705
|
+
app = FastAPI(
|
|
706
|
+
title=settings.app_name,
|
|
707
|
+
description="{context["description"]}",
|
|
708
|
+
version="{context["version"]}",
|
|
709
|
+
docs_url="/docs",
|
|
710
|
+
redoc_url="/redoc",
|
|
711
|
+
openapi_url="/openapi.json",
|
|
712
|
+
{lifespan_arg}
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
# CORS middleware
|
|
716
|
+
app.add_middleware(
|
|
717
|
+
CORSMiddleware,
|
|
718
|
+
allow_origins=["*"],
|
|
719
|
+
allow_credentials=True,
|
|
720
|
+
allow_methods=["*"],
|
|
721
|
+
allow_headers=["*"],
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
# Setup exception handlers
|
|
725
|
+
setup_exception_handlers(app)
|
|
726
|
+
|
|
727
|
+
# Include API routers
|
|
728
|
+
app.include_router(v1_router, prefix=settings.api_v1_str)
|
|
729
|
+
|
|
730
|
+
return app
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
app = create_app()
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
if __name__ == "__main__":
|
|
737
|
+
import uvicorn
|
|
738
|
+
|
|
739
|
+
uvicorn.run(
|
|
740
|
+
"app.main:app",
|
|
741
|
+
host="0.0.0.0",
|
|
742
|
+
port=8000,
|
|
743
|
+
reload=settings.debug,
|
|
744
|
+
)
|
|
745
|
+
'''
|
|
746
|
+
(app_dir / "main.py").write_text(main_py, encoding="utf-8")
|