fastforge-cli 0.0.1__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.
Files changed (76) hide show
  1. fastforge/__init__.py +1 -0
  2. fastforge/cli.py +693 -0
  3. fastforge/infra_template/cookiecutter.json +11 -0
  4. fastforge/infra_template/hooks/post_gen_project.py +77 -0
  5. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/README.md +41 -0
  6. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.app.yml +27 -0
  7. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.elasticsearch.yml +32 -0
  8. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.fluentbit.yml +14 -0
  9. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.kafka.yml +20 -0
  10. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.logstash.yml +14 -0
  11. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.mongodb.yml +17 -0
  12. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.postgres.yml +21 -0
  13. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.vault.yml +19 -0
  14. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.vector-agent.yml +13 -0
  15. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.vector-aggregator.yml +9 -0
  16. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/docker-compose.yml +31 -0
  17. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/fluentbit/fluent-bit.conf +24 -0
  18. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/fluentbit/parsers.conf +5 -0
  19. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/logstash/pipeline/logstash.conf +31 -0
  20. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/vault/config.hcl +10 -0
  21. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/vault/policies/app-policy.hcl +7 -0
  22. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/vector/vector-agent.toml +36 -0
  23. fastforge/infra_template/{{cookiecutter.project_slug}}-infrastructure/vector/vector-aggregator.toml +29 -0
  24. fastforge/template/cookiecutter.json +34 -0
  25. fastforge/template/hooks/post_gen_project.py +71 -0
  26. fastforge/template/{{cookiecutter.project_slug}}/.codeclimate.yml +40 -0
  27. fastforge/template/{{cookiecutter.project_slug}}/.dockerignore +15 -0
  28. fastforge/template/{{cookiecutter.project_slug}}/.env.staging +86 -0
  29. fastforge/template/{{cookiecutter.project_slug}}/.gitignore +15 -0
  30. fastforge/template/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +16 -0
  31. fastforge/template/{{cookiecutter.project_slug}}/Dockerfile +30 -0
  32. fastforge/template/{{cookiecutter.project_slug}}/README.md +254 -0
  33. fastforge/template/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
  34. fastforge/template/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
  35. fastforge/template/{{cookiecutter.project_slug}}/app/api/exception_handlers.py +78 -0
  36. fastforge/template/{{cookiecutter.project_slug}}/app/api/models/__init__.py +0 -0
  37. fastforge/template/{{cookiecutter.project_slug}}/app/api/models/{{cookiecutter.model_name}}.py +22 -0
  38. fastforge/template/{{cookiecutter.project_slug}}/app/api/routes/__init__.py +0 -0
  39. fastforge/template/{{cookiecutter.project_slug}}/app/api/routes/health.py +20 -0
  40. fastforge/template/{{cookiecutter.project_slug}}/app/api/routes/{{cookiecutter.model_name_plural}}.py +70 -0
  41. fastforge/template/{{cookiecutter.project_slug}}/app/cache.py +63 -0
  42. fastforge/template/{{cookiecutter.project_slug}}/app/config.py +112 -0
  43. fastforge/template/{{cookiecutter.project_slug}}/app/db/__init__.py +0 -0
  44. fastforge/template/{{cookiecutter.project_slug}}/app/db/models/__init__.py +0 -0
  45. fastforge/template/{{cookiecutter.project_slug}}/app/db/models/{{cookiecutter.model_name}}.py +34 -0
  46. fastforge/template/{{cookiecutter.project_slug}}/app/db/mongodb.py +23 -0
  47. fastforge/template/{{cookiecutter.project_slug}}/app/db/sqlalchemy.py +14 -0
  48. fastforge/template/{{cookiecutter.project_slug}}/app/dependencies.py +45 -0
  49. fastforge/template/{{cookiecutter.project_slug}}/app/logging_config.py +84 -0
  50. fastforge/template/{{cookiecutter.project_slug}}/app/main.py +106 -0
  51. fastforge/template/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
  52. fastforge/template/{{cookiecutter.project_slug}}/app/middleware/logging_middleware.py +45 -0
  53. fastforge/template/{{cookiecutter.project_slug}}/app/middleware/security_headers.py +18 -0
  54. fastforge/template/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
  55. fastforge/template/{{cookiecutter.project_slug}}/app/repositories/{{cookiecutter.model_name}}_repository.py +172 -0
  56. fastforge/template/{{cookiecutter.project_slug}}/app/secrets.py +101 -0
  57. fastforge/template/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
  58. fastforge/template/{{cookiecutter.project_slug}}/app/services/{{cookiecutter.model_name}}_service.py +63 -0
  59. fastforge/template/{{cookiecutter.project_slug}}/app/streaming/__init__.py +0 -0
  60. fastforge/template/{{cookiecutter.project_slug}}/app/streaming/consumer.py +187 -0
  61. fastforge/template/{{cookiecutter.project_slug}}/app/streaming/handler.py +31 -0
  62. fastforge/template/{{cookiecutter.project_slug}}/app/streaming/producer.py +151 -0
  63. fastforge/template/{{cookiecutter.project_slug}}/docker-compose.debug.yml +15 -0
  64. fastforge/template/{{cookiecutter.project_slug}}/docker-compose.yml +141 -0
  65. fastforge/template/{{cookiecutter.project_slug}}/pyproject.toml +100 -0
  66. fastforge/template/{{cookiecutter.project_slug}}/qodana.yaml +22 -0
  67. fastforge/template/{{cookiecutter.project_slug}}/sonar-project.properties +20 -0
  68. fastforge/template/{{cookiecutter.project_slug}}/tests/__init__.py +0 -0
  69. fastforge/template/{{cookiecutter.project_slug}}/tests/conftest.py +11 -0
  70. fastforge/template/{{cookiecutter.project_slug}}/tests/test_api.py +51 -0
  71. fastforge_cli-0.0.1.dist-info/METADATA +163 -0
  72. fastforge_cli-0.0.1.dist-info/RECORD +76 -0
  73. fastforge_cli-0.0.1.dist-info/WHEEL +5 -0
  74. fastforge_cli-0.0.1.dist-info/entry_points.txt +12 -0
  75. fastforge_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
  76. fastforge_cli-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,15 @@
1
+ **/__pycache__
2
+ **/.git
3
+ **/.gitignore
4
+ **/.env
5
+ **/.env.*
6
+ **/.vscode
7
+ **/.pytest_cache
8
+ **/.coverage
9
+ **/htmlcov
10
+ **/docker-compose*
11
+ **/Dockerfile*
12
+ **/*.md
13
+ **/tests
14
+ **/.ruff_cache
15
+ **/.pre-commit-config.yaml
@@ -0,0 +1,86 @@
1
+ # {{ cookiecutter.project_name }} — Staging
2
+ APP_NAME={{ cookiecutter.project_slug }}
3
+ APP_ENV=staging
4
+ APP_DEBUG=true
5
+ APP_PORT={{ cookiecutter.port }}
6
+ APP_LOG_LEVEL=debug
7
+ {%- if cookiecutter.logging == "structlog" %}
8
+
9
+ # Logging
10
+ LOG_FORMAT={{ cookiecutter.log_format }}
11
+ LOG_FILE_ENABLED=false
12
+ {%- endif %}
13
+ {%- if cookiecutter.database == "postgres" %}
14
+
15
+ # Database
16
+ DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/{{ cookiecutter.package_name }}
17
+ {%- endif %}
18
+ {%- if cookiecutter.database == "mysql" %}
19
+
20
+ # Database
21
+ DATABASE_URL=mysql+aiomysql://root:root@mysql:3306/{{ cookiecutter.package_name }}
22
+ {%- endif %}
23
+ {%- if cookiecutter.database == "sqlite" %}
24
+
25
+ # Database
26
+ DATABASE_URL=sqlite+aiosqlite:///./{{ cookiecutter.package_name }}.db
27
+ {%- endif %}
28
+ {%- if cookiecutter.database == "mongodb" %}
29
+
30
+ # Database
31
+ MONGODB_URL=mongodb://mongodb:27017
32
+ MONGODB_DATABASE={{ cookiecutter.package_name }}
33
+ {%- endif %}
34
+ {%- if cookiecutter.cache == "redis" or cookiecutter.streaming == "redis_pubsub" %}
35
+
36
+ # Redis
37
+ REDIS_URL=redis://redis:6379/0
38
+ {%- endif %}
39
+ {%- if cookiecutter.cache == "memcached" %}
40
+
41
+ # Memcached
42
+ MEMCACHED_HOST=memcached
43
+ MEMCACHED_PORT=11211
44
+ {%- endif %}
45
+ {%- if cookiecutter.streaming == "kafka" %}
46
+
47
+ # Kafka
48
+ KAFKA_BOOTSTRAP_SERVERS=kafka:9092
49
+ KAFKA_TOPIC={{ cookiecutter.project_slug }}-events
50
+ KAFKA_GROUP_ID={{ cookiecutter.project_slug }}-group
51
+ {%- endif %}
52
+ {%- if cookiecutter.streaming == "rabbitmq" %}
53
+
54
+ # RabbitMQ
55
+ RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672/
56
+ {%- endif %}
57
+ {%- if cookiecutter.streaming == "nats" %}
58
+
59
+ # NATS
60
+ NATS_URL=nats://nats:4222
61
+ {%- endif %}
62
+ {%- if cookiecutter.secrets == "vault" %}
63
+
64
+ # Vault
65
+ VAULT_URL=http://vault:8200
66
+ VAULT_TOKEN=dev-only-token
67
+ VAULT_MOUNT_POINT=secret
68
+ VAULT_SECRET_PATH={{ cookiecutter.project_slug }}
69
+ {%- endif %}
70
+ {%- if cookiecutter.secrets == "aws_sm" %}
71
+
72
+ # AWS Secrets Manager
73
+ AWS_REGION=us-east-1
74
+ AWS_SECRET_NAME={{ cookiecutter.project_slug }}
75
+ {%- endif %}
76
+ {%- if cookiecutter.secrets == "azure_kv" %}
77
+
78
+ # Azure Key Vault
79
+ AZURE_VAULT_URL=https://your-vault.vault.azure.net
80
+ {%- endif %}
81
+ {%- if cookiecutter.secrets == "gcp_sm" %}
82
+
83
+ # GCP Secret Manager
84
+ GCP_PROJECT_ID=your-project-id
85
+ GCP_SECRET_NAME={{ cookiecutter.project_slug }}
86
+ {%- endif %}
@@ -0,0 +1,15 @@
1
+ .env
2
+ .env.local
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .venv/
10
+ venv/
11
+ .pytest_cache/
12
+ .coverage
13
+ htmlcov/
14
+ *.log
15
+ .ruff_cache/
@@ -0,0 +1,16 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.8.0
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
9
+ - repo: local
10
+ hooks:
11
+ - id: pytest
12
+ name: pytest
13
+ entry: pytest tests/ -x -q
14
+ language: system
15
+ pass_filenames: false
16
+ always_run: true
@@ -0,0 +1,30 @@
1
+ FROM python:{{cookiecutter.python_version}}-slim AS base
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ curl \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ COPY pyproject.toml .
10
+ RUN pip install --no-cache-dir .
11
+
12
+ COPY app/ app/
13
+ {%- if cookiecutter.log_connector == "file" %}
14
+
15
+ RUN mkdir -p /var/log/app
16
+ {%- endif %}
17
+
18
+ RUN adduser --disabled-password --gecos "" appuser \
19
+ {%- if cookiecutter.log_connector == "file" %}
20
+ && chown -R appuser:appuser /var/log/app
21
+ {%- endif %}
22
+
23
+ USER appuser
24
+
25
+ EXPOSE {{cookiecutter.port}}
26
+
27
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
28
+ CMD curl -f http://localhost:{{cookiecutter.port}}/health || exit 1
29
+
30
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "{{cookiecutter.port}}", "--log-level", "info"]
@@ -0,0 +1,254 @@
1
+ # {{ cookiecutter.project_name }}
2
+
3
+ > {{ cookiecutter.description }}
4
+
5
+ **Author:** {{ cookiecutter.author_name }}
6
+ **Python:** {{ cookiecutter.python_version }}
7
+ **Port:** {{ cookiecutter.port }}
8
+
9
+ ---
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Install dependencies
15
+ pip install -e ".[dev]"
16
+
17
+ # Run development server
18
+ uvicorn app.main:app --reload --port {{ cookiecutter.port }}
19
+ ```
20
+ {%- if cookiecutter.docker == "yes" %}
21
+
22
+ ### With Docker
23
+
24
+ ```bash
25
+ # Start all services
26
+ docker compose up --build
27
+
28
+ # Logs
29
+ docker compose logs -f app
30
+ ```
31
+ {%- if cookiecutter.docker_debug == "yes" %}
32
+
33
+ ### Debug Mode (VS Code)
34
+
35
+ ```bash
36
+ docker compose -f docker-compose.debug.yml up
37
+ # Attach VS Code debugger to port 5678
38
+ ```
39
+ {%- endif %}
40
+ {%- endif %}
41
+
42
+ ## API Endpoints
43
+
44
+ | Method | Path | Description |
45
+ |--------|------|-------------|
46
+ | `GET` | `/health` | Health check |
47
+ | `GET` | `/api/v1/{{ cookiecutter.model_name_plural }}` | List {{ cookiecutter.model_name_plural }} |
48
+ | `POST` | `/api/v1/{{ cookiecutter.model_name_plural }}` | Create {{ cookiecutter.model_name }} |
49
+ | `GET` | `/api/v1/{{ cookiecutter.model_name_plural }}/{id}` | Get {{ cookiecutter.model_name }} |
50
+ | `PUT` | `/api/v1/{{ cookiecutter.model_name_plural }}/{id}` | Update {{ cookiecutter.model_name }} |
51
+ | `DELETE` | `/api/v1/{{ cookiecutter.model_name_plural }}/{id}` | Delete {{ cookiecutter.model_name }} |
52
+
53
+ ## Architecture (SOLID)
54
+
55
+ ```
56
+ app/
57
+ ├── main.py # App factory, middleware
58
+ ├── config.py # Settings (pydantic-settings)
59
+ ├── dependencies.py # FastAPI DI wiring
60
+ ├── api/
61
+ │ ├── exception_handlers.py # Structured error responses
62
+ │ ├── routes/
63
+ │ │ ├── health.py # Health + readiness
64
+ │ │ └── {{ cookiecutter.model_name_plural }}.py # CRUD routes
65
+ │ └── models/
66
+ │ └── {{ cookiecutter.model_name }}.py # Pydantic schemas
67
+ ├── services/
68
+ │ └── {{ cookiecutter.model_name }}_service.py # Business logic
69
+ ├── repositories/
70
+ │ └── {{ cookiecutter.model_name }}_repository.py # Data access
71
+ {%- if cookiecutter.database != "none" %}
72
+ ├── db/models/
73
+ │ └── {{ cookiecutter.model_name }}.py # DB model
74
+ {%- endif %}
75
+ └── middleware/
76
+ ├── security_headers.py # Security headers
77
+ └── logging_middleware.py # Request logging
78
+ ```
79
+
80
+ ## Tests
81
+
82
+ ```bash
83
+ pytest tests/ -v
84
+ pytest tests/ --cov=app --cov-report=html
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Features
90
+ {%- if cookiecutter.logging == "structlog" %}
91
+
92
+ ### Structured Logging
93
+
94
+ - **Library:** structlog
95
+ - **Format:** {{ cookiecutter.log_format }}
96
+ {%- if cookiecutter.log_connector != "stdout" %}
97
+ - **Connector:** {{ cookiecutter.log_connector }}
98
+ {%- endif %}
99
+ - Request correlation IDs via `X-Request-ID` header
100
+ {%- endif %}
101
+ {%- if cookiecutter.database == "postgres" %}
102
+
103
+ ### Database (PostgreSQL)
104
+
105
+ - **Driver:** asyncpg via SQLAlchemy async
106
+ - Configure via `DATABASE_URL`
107
+ {%- endif %}
108
+ {%- if cookiecutter.database == "mysql" %}
109
+
110
+ ### Database (MySQL)
111
+
112
+ - **Driver:** aiomysql via SQLAlchemy async
113
+ - Configure via `DATABASE_URL`
114
+ {%- endif %}
115
+ {%- if cookiecutter.database == "sqlite" %}
116
+
117
+ ### Database (SQLite)
118
+
119
+ - **Driver:** aiosqlite via SQLAlchemy async
120
+ - Configure via `DATABASE_URL`
121
+ {%- endif %}
122
+ {%- if cookiecutter.database == "mongodb" %}
123
+
124
+ ### Database (MongoDB)
125
+
126
+ - **Driver:** Motor (async)
127
+ - Configure via `MONGODB_URL`, `MONGODB_DATABASE`
128
+ {%- endif %}
129
+ {%- if cookiecutter.cache == "redis" %}
130
+
131
+ ### Cache (Redis)
132
+
133
+ - **Library:** redis-py (async)
134
+ - Configure via `REDIS_URL`
135
+ - Usage: `from app.cache import get_cache`
136
+ {%- endif %}
137
+ {%- if cookiecutter.cache == "memcached" %}
138
+
139
+ ### Cache (Memcached)
140
+
141
+ - **Library:** aiomcache
142
+ - Configure via `MEMCACHED_HOST`, `MEMCACHED_PORT`
143
+ - Usage: `from app.cache import get_cache`
144
+ {%- endif %}
145
+ {%- if cookiecutter.cache == "in_memory" %}
146
+
147
+ ### Cache (In-Memory)
148
+
149
+ - **Library:** cachetools (TTLCache)
150
+ - 1024 items max, 5 min TTL
151
+ - Usage: `from app.cache import get_cache`
152
+ {%- endif %}
153
+ {%- if cookiecutter.streaming == "kafka" %}
154
+
155
+ ### Streaming (Kafka)
156
+
157
+ - **Library:** aiokafka
158
+ - Producer: `from app.streaming.producer import send_event`
159
+ - Consumer auto-starts in lifespan, dispatches to `app/streaming/handler.py`
160
+ - Configure via `KAFKA_BOOTSTRAP_SERVERS`, `KAFKA_GROUP_ID`
161
+ {%- endif %}
162
+ {%- if cookiecutter.streaming == "rabbitmq" %}
163
+
164
+ ### Streaming (RabbitMQ)
165
+
166
+ - **Library:** aio-pika
167
+ - Producer: `from app.streaming.producer import send_event`
168
+ - Consumer auto-starts in lifespan, dispatches to `app/streaming/handler.py`
169
+ - Configure via `RABBITMQ_URL`
170
+ {%- endif %}
171
+ {%- if cookiecutter.streaming == "redis_pubsub" %}
172
+
173
+ ### Streaming (Redis Pub/Sub)
174
+
175
+ - **Library:** redis-py (async)
176
+ - Producer: `from app.streaming.producer import send_event`
177
+ - Consumer auto-starts in lifespan, dispatches to `app/streaming/handler.py`
178
+ - Configure via `REDIS_URL`
179
+ {%- endif %}
180
+ {%- if cookiecutter.streaming == "nats" %}
181
+
182
+ ### Streaming (NATS)
183
+
184
+ - **Library:** nats-py
185
+ - Producer: `from app.streaming.producer import send_event`
186
+ - Consumer auto-starts in lifespan, dispatches to `app/streaming/handler.py`
187
+ - Configure via `NATS_URL`
188
+ {%- endif %}
189
+ {%- if cookiecutter.secrets == "vault" %}
190
+
191
+ ### Secrets (HashiCorp Vault)
192
+
193
+ - **Library:** hvac
194
+ - Secrets loaded at startup into `app.state.secrets`
195
+ - Configure via `VAULT_URL`, `VAULT_TOKEN`
196
+ {%- endif %}
197
+ {%- if cookiecutter.secrets == "aws_sm" %}
198
+
199
+ ### Secrets (AWS Secrets Manager)
200
+
201
+ - **Library:** boto3
202
+ - Secrets loaded at startup into `app.state.secrets`
203
+ - Configure via `AWS_REGION`, `AWS_SECRET_NAME`
204
+ {%- endif %}
205
+ {%- if cookiecutter.secrets == "azure_kv" %}
206
+
207
+ ### Secrets (Azure Key Vault)
208
+
209
+ - **Library:** azure-keyvault-secrets + azure-identity
210
+ - Secrets loaded at startup into `app.state.secrets`
211
+ - Configure via `AZURE_VAULT_URL`
212
+ {%- endif %}
213
+ {%- if cookiecutter.secrets == "gcp_sm" %}
214
+
215
+ ### Secrets (GCP Secret Manager)
216
+
217
+ - **Library:** google-cloud-secret-manager
218
+ - Secrets loaded at startup into `app.state.secrets`
219
+ - Configure via `GCP_PROJECT_ID`, `GCP_SECRET_NAME`
220
+ {%- endif %}
221
+ {%- if cookiecutter.quality_gate != "none" %}
222
+
223
+ ### Quality Gate
224
+
225
+ - **Tool:** {{ cookiecutter.quality_gate }}
226
+ {%- endif %}
227
+ {%- if cookiecutter.docker == "yes" %}
228
+
229
+ ### Containerization
230
+
231
+ - Multi-stage Dockerfile (slim base)
232
+ - Non-root user
233
+ - Health check built-in
234
+ {%- endif %}
235
+
236
+ ## Environment
237
+
238
+ Configuration is managed via environment variables. See `.env.staging`.
239
+
240
+ ## Extend Your Project
241
+
242
+ ```bash
243
+ fastforge-infra # Infrastructure (Kafka, ES, Vault, DB)
244
+ fastforge-cicd # CI/CD pipeline
245
+ fastforge-secops # Security tools
246
+ fastforge-helm # Helm chart
247
+ fastforge-k8s # Kubernetes manifests
248
+ fastforge-swarm # Docker Swarm stack
249
+ fastforge-observability # Tracing + Metrics
250
+ ```
251
+
252
+ ---
253
+
254
+ *Generated with [FastForge](https://github.com/jinnabaalu/fastforge)*
@@ -0,0 +1,78 @@
1
+ """Structured exception handlers for consistent API error responses."""
2
+
3
+ import structlog
4
+ from fastapi import FastAPI, Request
5
+ from fastapi.exceptions import RequestValidationError
6
+ from fastapi.responses import JSONResponse
7
+ from starlette.exceptions import HTTPException as StarletteHTTPException
8
+
9
+ logger = structlog.get_logger(__name__)
10
+
11
+
12
+ async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
13
+ """Handle HTTP exceptions with a consistent JSON structure."""
14
+ await logger.awarning(
15
+ "http_error",
16
+ status_code=exc.status_code,
17
+ detail=exc.detail,
18
+ path=request.url.path,
19
+ )
20
+ return JSONResponse(
21
+ status_code=exc.status_code,
22
+ content={
23
+ "error": {
24
+ "code": exc.status_code,
25
+ "message": exc.detail,
26
+ "path": request.url.path,
27
+ }
28
+ },
29
+ )
30
+
31
+
32
+ async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
33
+ """Handle request validation errors with field-level details."""
34
+ await logger.awarning(
35
+ "validation_error",
36
+ path=request.url.path,
37
+ errors=exc.errors(),
38
+ )
39
+ return JSONResponse(
40
+ status_code=422,
41
+ content={
42
+ "error": {
43
+ "code": 422,
44
+ "message": "Validation error",
45
+ "path": request.url.path,
46
+ "details": [
47
+ {"field": ".".join(str(loc) for loc in e["loc"]), "message": e["msg"]}
48
+ for e in exc.errors()
49
+ ],
50
+ }
51
+ },
52
+ )
53
+
54
+
55
+ async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
56
+ """Catch-all for unhandled exceptions — never leak stack traces."""
57
+ await logger.aexception(
58
+ "unhandled_error",
59
+ path=request.url.path,
60
+ exc_type=type(exc).__name__,
61
+ )
62
+ return JSONResponse(
63
+ status_code=500,
64
+ content={
65
+ "error": {
66
+ "code": 500,
67
+ "message": "Internal server error",
68
+ "path": request.url.path,
69
+ }
70
+ },
71
+ )
72
+
73
+
74
+ def register_exception_handlers(app: FastAPI) -> None:
75
+ """Register all exception handlers on the app."""
76
+ app.add_exception_handler(StarletteHTTPException, http_exception_handler)
77
+ app.add_exception_handler(RequestValidationError, validation_exception_handler)
78
+ app.add_exception_handler(Exception, unhandled_exception_handler)
@@ -0,0 +1,22 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class {{ cookiecutter.model_name_class }}Create(BaseModel):
5
+ name: str = Field(..., min_length=1, max_length=255)
6
+ description: str | None = Field(None, max_length=1000)
7
+
8
+
9
+ class {{ cookiecutter.model_name_class }}Update(BaseModel):
10
+ name: str | None = Field(None, min_length=1, max_length=255)
11
+ description: str | None = Field(None, max_length=1000)
12
+
13
+
14
+ class {{ cookiecutter.model_name_class }}Response(BaseModel):
15
+ id: str
16
+ name: str
17
+ description: str | None
18
+
19
+
20
+ class {{ cookiecutter.model_name_class }}ListResponse(BaseModel):
21
+ {{ cookiecutter.model_name_plural }}: list[{{ cookiecutter.model_name_class }}Response]
22
+ total: int
@@ -0,0 +1,20 @@
1
+ from fastapi import APIRouter
2
+
3
+ from app.config import settings
4
+
5
+ router = APIRouter(tags=["health"])
6
+
7
+
8
+ @router.get("/health")
9
+ async def health_check() -> dict:
10
+ return {
11
+ "status": "healthy",
12
+ "service": settings.app_name,
13
+ "version": settings.app_version,
14
+ "environment": settings.app_env,
15
+ }
16
+
17
+
18
+ @router.get("/ready")
19
+ async def readiness_check() -> dict:
20
+ return {"status": "ready"}
@@ -0,0 +1,70 @@
1
+ {% if cookiecutter.logging == "structlog" -%}
2
+ from app.logging_config import get_logger
3
+ {%- else -%}
4
+ from logging import getLogger
5
+ {%- endif %}
6
+ from fastapi import APIRouter, Depends, HTTPException
7
+
8
+ from app.api.models.{{cookiecutter.model_name}} import (
9
+ {{cookiecutter.model_name_class}}Create,
10
+ {{cookiecutter.model_name_class}}ListResponse,
11
+ {{cookiecutter.model_name_class}}Response,
12
+ {{cookiecutter.model_name_class}}Update,
13
+ )
14
+ from app.dependencies import get_{{cookiecutter.model_name}}_service
15
+ from app.services.{{cookiecutter.model_name}}_service import {{cookiecutter.model_name_class}}Service
16
+
17
+ router = APIRouter(prefix="/api/v1/{{cookiecutter.model_name_plural}}", tags=["{{cookiecutter.model_name_plural}}"])
18
+ {% if cookiecutter.logging == "structlog" -%}
19
+ logger = get_logger(__name__)
20
+ {%- else -%}
21
+ logger = getLogger(__name__)
22
+ {%- endif %}
23
+
24
+
25
+ @router.post("/", response_model={{cookiecutter.model_name_class}}Response, status_code=201)
26
+ async def create_{{cookiecutter.model_name}}(
27
+ data: {{cookiecutter.model_name_class}}Create,
28
+ service: {{cookiecutter.model_name_class}}Service = Depends(get_{{cookiecutter.model_name}}_service),
29
+ ) -> {{cookiecutter.model_name_class}}Response:
30
+ return await service.create(data)
31
+
32
+
33
+ @router.get("/", response_model={{cookiecutter.model_name_class}}ListResponse)
34
+ async def list_{{cookiecutter.model_name_plural}}(
35
+ service: {{cookiecutter.model_name_class}}Service = Depends(get_{{cookiecutter.model_name}}_service),
36
+ ) -> {{cookiecutter.model_name_class}}ListResponse:
37
+ return await service.list_all()
38
+
39
+
40
+ @router.get("/{item_id}", response_model={{cookiecutter.model_name_class}}Response)
41
+ async def get_{{cookiecutter.model_name}}(
42
+ item_id: str,
43
+ service: {{cookiecutter.model_name_class}}Service = Depends(get_{{cookiecutter.model_name}}_service),
44
+ ) -> {{cookiecutter.model_name_class}}Response:
45
+ result = await service.get_by_id(item_id)
46
+ if not result:
47
+ raise HTTPException(status_code=404, detail="{{cookiecutter.model_name_class}} not found")
48
+ return result
49
+
50
+
51
+ @router.put("/{item_id}", response_model={{cookiecutter.model_name_class}}Response)
52
+ async def update_{{cookiecutter.model_name}}(
53
+ item_id: str,
54
+ data: {{cookiecutter.model_name_class}}Update,
55
+ service: {{cookiecutter.model_name_class}}Service = Depends(get_{{cookiecutter.model_name}}_service),
56
+ ) -> {{cookiecutter.model_name_class}}Response:
57
+ result = await service.update(item_id, data)
58
+ if not result:
59
+ raise HTTPException(status_code=404, detail="{{cookiecutter.model_name_class}} not found")
60
+ return result
61
+
62
+
63
+ @router.delete("/{item_id}", status_code=204)
64
+ async def delete_{{cookiecutter.model_name}}(
65
+ item_id: str,
66
+ service: {{cookiecutter.model_name_class}}Service = Depends(get_{{cookiecutter.model_name}}_service),
67
+ ) -> None:
68
+ deleted = await service.delete(item_id)
69
+ if not deleted:
70
+ raise HTTPException(status_code=404, detail="{{cookiecutter.model_name_class}} not found")
@@ -0,0 +1,63 @@
1
+ """Cache client — {{ cookiecutter.cache }} backend."""
2
+ {%- if cookiecutter.cache == "redis" %}
3
+
4
+ import redis.asyncio as redis
5
+
6
+ from app.config import settings
7
+
8
+ _client: redis.Redis | None = None
9
+
10
+
11
+ async def get_cache() -> redis.Redis:
12
+ """Get the Redis cache client (lazy init)."""
13
+ global _client # noqa: PLW0603
14
+ if _client is None:
15
+ _client = redis.from_url(settings.redis_url, decode_responses=True)
16
+ return _client
17
+
18
+
19
+ async def close_cache() -> None:
20
+ """Close the Redis connection pool."""
21
+ global _client # noqa: PLW0603
22
+ if _client is not None:
23
+ await _client.aclose()
24
+ _client = None
25
+ {%- elif cookiecutter.cache == "memcached" %}
26
+
27
+ import aiomcache
28
+
29
+ from app.config import settings
30
+
31
+ _client: aiomcache.Client | None = None
32
+
33
+
34
+ def get_cache() -> aiomcache.Client:
35
+ """Get the Memcached client (lazy init)."""
36
+ global _client # noqa: PLW0603
37
+ if _client is None:
38
+ _client = aiomcache.Client(settings.memcached_host, settings.memcached_port)
39
+ return _client
40
+
41
+
42
+ async def close_cache() -> None:
43
+ """Close the Memcached connection."""
44
+ global _client # noqa: PLW0603
45
+ if _client is not None:
46
+ await _client.close()
47
+ _client = None
48
+ {%- elif cookiecutter.cache == "in_memory" %}
49
+
50
+ from cachetools import TTLCache
51
+
52
+ _cache: TTLCache = TTLCache(maxsize=1024, ttl=300)
53
+
54
+
55
+ def get_cache() -> TTLCache:
56
+ """Get the in-memory TTL cache (1024 items, 5 min TTL)."""
57
+ return _cache
58
+
59
+
60
+ async def close_cache() -> None:
61
+ """Clear the in-memory cache."""
62
+ _cache.clear()
63
+ {%- endif %}