fastapi-spawn 0.2.0__tar.gz → 0.3.0__tar.gz
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_spawn-0.2.0 → fastapi_spawn-0.3.0}/PKG-INFO +1 -1
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/__init__.py +1 -1
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/config.py +15 -1
- fastapi_spawn-0.3.0/fastapi_spawn/constants.py +266 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/generator.py +5 -0
- fastapi_spawn-0.3.0/fastapi_spawn/templates/app/api/graphql.py.j2 +98 -0
- fastapi_spawn-0.3.0/fastapi_spawn/templates/app/api/v1/ws.py.j2 +67 -0
- fastapi_spawn-0.3.0/fastapi_spawn/templates/app/core/ai.py.j2 +143 -0
- fastapi_spawn-0.3.0/fastapi_spawn/templates/app/core/storage.py.j2 +121 -0
- fastapi_spawn-0.3.0/fastapi_spawn/templates/app/core/vector_db.py.j2 +118 -0
- fastapi_spawn-0.3.0/fastapi_spawn/templates/app/core/ws_manager.py.j2 +74 -0
- fastapi_spawn-0.3.0/fastapi_spawn/templates/base/env_example.j2 +300 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/pyproject.toml.j2 +37 -0
- fastapi_spawn-0.3.0/fastapi_spawn/templates/tasks/arq_worker.py.j2 +56 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/pyproject.toml +1 -1
- fastapi_spawn-0.2.0/fastapi_spawn/constants.py +0 -223
- fastapi_spawn-0.2.0/fastapi_spawn/templates/app/core/ai.py.j2 +0 -140
- fastapi_spawn-0.2.0/fastapi_spawn/templates/app/core/storage.py.j2 +0 -73
- fastapi_spawn-0.2.0/fastapi_spawn/templates/app/core/vector_db.py.j2 +0 -92
- fastapi_spawn-0.2.0/fastapi_spawn/templates/base/env_example.j2 +0 -243
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/.gitignore +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/CHANGELOG.md +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/LICENSE +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/README.md +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/cli.py +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/interactive.py +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/alembic/alembic.ini.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/alembic/env.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/__init__.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/api/deps.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/api/v1/auth.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/api/v1/health.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/config.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/email.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/exceptions.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/logger.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/logging.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/monitoring.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/notifications.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/security.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/db/session.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/main.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/middleware/__init__.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/middleware/rate_limit.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/middleware/request_logger.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/Makefile.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/README.md.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/env.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/gitignore.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/pre_commit.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/ci/github/publish.yml.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/ci/github/tests.yml.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/ci/gitlab/gitlab-ci.yml.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/docker/Dockerfile.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/docker/docker-compose.yml.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/docker/dockerignore.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/docker/docker-compose.prod.yml.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/helm/Chart.yaml.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/helm/values.yaml.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/terraform/main.tf.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/terraform/variables.tf.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/root/main.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/tasks/celery_app.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/tasks/sample_tasks.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/tests/conftest.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/tests/test_health.py.j2 +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/utils.py +0 -0
- {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-spawn
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A powerful CLI tool to scaffold production-ready FastAPI projects with flexible database, auth, broker, and deployment options.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Bishwajitgarai/fastapi-spawn
|
|
6
6
|
Project-URL: Documentation, https://github.com/Bishwajitgarai/fastapi-spawn#readme
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
|
|
7
7
|
from fastapi_spawn.constants import (
|
|
8
|
-
AIProvider, AuthType, Broker, Cache, CIProvider,
|
|
8
|
+
AIProvider, APIExtra, AuthType, Broker, Cache, CIProvider,
|
|
9
9
|
Database, EmailProvider, LogDestination, LogLibrary,
|
|
10
10
|
MigrationTool, MonitoringProvider, NotificationProvider,
|
|
11
11
|
ORM, Stack, Storage, VectorDB,
|
|
@@ -39,6 +39,8 @@ class ProjectConfig:
|
|
|
39
39
|
# Deployment
|
|
40
40
|
stack: Stack = Stack.standard
|
|
41
41
|
ci: CIProvider = CIProvider.github
|
|
42
|
+
# API extras
|
|
43
|
+
api_extra: APIExtra = APIExtra.none
|
|
42
44
|
# Flags
|
|
43
45
|
include_docker: bool = True
|
|
44
46
|
include_tests: bool = True
|
|
@@ -135,6 +137,14 @@ class ProjectConfig:
|
|
|
135
137
|
def has_log_file(self) -> bool:
|
|
136
138
|
return self.log_dest in (LogDestination.local, LogDestination.cloudwatch, LogDestination.datadog)
|
|
137
139
|
|
|
140
|
+
@property
|
|
141
|
+
def has_websockets(self) -> bool:
|
|
142
|
+
return self.api_extra in (APIExtra.websockets, APIExtra.both)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def has_graphql(self) -> bool:
|
|
146
|
+
return self.api_extra in (APIExtra.graphql, APIExtra.both)
|
|
147
|
+
|
|
138
148
|
# ── Template context ───────────────────────────────────────────────────
|
|
139
149
|
|
|
140
150
|
def to_context(self) -> dict:
|
|
@@ -159,6 +169,9 @@ class ProjectConfig:
|
|
|
159
169
|
"stack": self.stack.value,
|
|
160
170
|
"ci": self.ci.value,
|
|
161
171
|
# booleans
|
|
172
|
+
"api_extra": self.api_extra.value,
|
|
173
|
+
"has_websockets": self.has_websockets,
|
|
174
|
+
"has_graphql": self.has_graphql,
|
|
162
175
|
"has_relational_db": self.has_relational_db,
|
|
163
176
|
"has_mongo": self.has_mongo,
|
|
164
177
|
"has_auth": self.has_auth,
|
|
@@ -203,6 +216,7 @@ class ProjectConfig:
|
|
|
203
216
|
("Log dest", self.log_dest.value),
|
|
204
217
|
("Stack", self.stack.value),
|
|
205
218
|
("CI/CD", self.ci.value),
|
|
219
|
+
("API extras", self.api_extra.value),
|
|
206
220
|
("Docker", "yes" if self.has_docker else "no"),
|
|
207
221
|
("Tests", "yes" if self.include_tests else "no"),
|
|
208
222
|
("Dry-run", "yes" if self.dry_run else "no"),
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Constants and enums for fastapi-spawn — the complete FastAPI ecosystem."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# ── Database ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
class Database(str, Enum):
|
|
9
|
+
postgresql = "postgresql"
|
|
10
|
+
mysql = "mysql"
|
|
11
|
+
mongodb = "mongodb"
|
|
12
|
+
sqlite = "sqlite"
|
|
13
|
+
supabase = "supabase" # managed Postgres + realtime
|
|
14
|
+
duckdb = "duckdb" # analytical / OLAP queries
|
|
15
|
+
none = "none"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ORM(str, Enum):
|
|
19
|
+
sqlalchemy = "sqlalchemy"
|
|
20
|
+
sqlmodel = "sqlmodel" # Pydantic v2 + SQLAlchemy — type-safe
|
|
21
|
+
tortoise = "tortoise"
|
|
22
|
+
beanie = "beanie" # MongoDB ODM
|
|
23
|
+
none = "none"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MigrationTool(str, Enum):
|
|
27
|
+
alembic = "alembic"
|
|
28
|
+
aerich = "aerich"
|
|
29
|
+
none = "none"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── Auth ───────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
class AuthType(str, Enum):
|
|
35
|
+
jwt = "jwt"
|
|
36
|
+
oauth2 = "oauth2"
|
|
37
|
+
api_key = "api-key"
|
|
38
|
+
auth0 = "auth0" # managed identity (Auth0 / Okta)
|
|
39
|
+
none = "none"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Messaging ──────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
class Broker(str, Enum):
|
|
45
|
+
redis = "redis" # Celery + Redis
|
|
46
|
+
rabbitmq = "rabbitmq" # Celery + RabbitMQ
|
|
47
|
+
kafka = "kafka" # aiokafka
|
|
48
|
+
arq = "arq" # lightweight async Redis task queue
|
|
49
|
+
none = "none"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Cache(str, Enum):
|
|
53
|
+
redis = "redis"
|
|
54
|
+
memcached = "memcached"
|
|
55
|
+
none = "none"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── Storage ────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
class Storage(str, Enum):
|
|
61
|
+
s3 = "s3" # AWS S3 or MinIO
|
|
62
|
+
gcs = "gcs" # Google Cloud Storage
|
|
63
|
+
cloudinary = "cloudinary" # image/video CDN
|
|
64
|
+
local = "local"
|
|
65
|
+
none = "none"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ── AI / LLM ──────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
class AIProvider(str, Enum):
|
|
71
|
+
openai = "openai"
|
|
72
|
+
anthropic = "anthropic"
|
|
73
|
+
gemini = "gemini"
|
|
74
|
+
ollama = "ollama" # local, no API key
|
|
75
|
+
langchain = "langchain" # framework (wraps any LLM)
|
|
76
|
+
llamaindex = "llamaindex" # RAG-focused framework
|
|
77
|
+
none = "none"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ── Monitoring ─────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
class MonitoringProvider(str, Enum):
|
|
83
|
+
sentry = "sentry"
|
|
84
|
+
prometheus = "prometheus"
|
|
85
|
+
opentelemetry = "opentelemetry" # vendor-neutral OTEL tracing
|
|
86
|
+
both = "both" # sentry + prometheus
|
|
87
|
+
none = "none"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ── Email ─────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
class EmailProvider(str, Enum):
|
|
93
|
+
sendgrid = "sendgrid"
|
|
94
|
+
smtp = "smtp"
|
|
95
|
+
ses = "ses"
|
|
96
|
+
none = "none"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ── Notifications ──────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
class NotificationProvider(str, Enum):
|
|
102
|
+
slack = "slack"
|
|
103
|
+
discord = "discord"
|
|
104
|
+
none = "none"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ── Logging ────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
class LogLibrary(str, Enum):
|
|
110
|
+
loguru = "loguru"
|
|
111
|
+
structlog = "structlog"
|
|
112
|
+
standard = "standard"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class LogDestination(str, Enum):
|
|
116
|
+
local = "local"
|
|
117
|
+
cloudwatch = "cloudwatch" # AWS CloudWatch Logs
|
|
118
|
+
datadog = "datadog"
|
|
119
|
+
none = "none"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── Vector database ────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
class VectorDB(str, Enum):
|
|
125
|
+
qdrant = "qdrant"
|
|
126
|
+
chroma = "chroma" # ChromaDB — local dev
|
|
127
|
+
pinecone = "pinecone"
|
|
128
|
+
supabase = "supabase" # pgvector on Supabase
|
|
129
|
+
elasticsearch = "elasticsearch"
|
|
130
|
+
none = "none"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── Deployment ─────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
class Stack(str, Enum):
|
|
136
|
+
minimal = "minimal"
|
|
137
|
+
standard = "standard"
|
|
138
|
+
full = "full"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CIProvider(str, Enum):
|
|
142
|
+
github = "github"
|
|
143
|
+
gitlab = "gitlab"
|
|
144
|
+
both = "both"
|
|
145
|
+
none = "none"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class APIExtra(str, Enum):
|
|
149
|
+
websockets = "websockets" # WebSocket endpoint + connection manager
|
|
150
|
+
graphql = "graphql" # Strawberry GraphQL schema + router
|
|
151
|
+
both = "both" # WebSockets + GraphQL
|
|
152
|
+
none = "none"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ── Compatibility matrices ─────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
ORM_DB_COMPAT: dict[str, list[str]] = {
|
|
158
|
+
ORM.sqlalchemy: [Database.postgresql, Database.mysql, Database.sqlite, Database.supabase],
|
|
159
|
+
ORM.sqlmodel: [Database.postgresql, Database.mysql, Database.sqlite, Database.supabase],
|
|
160
|
+
ORM.tortoise: [Database.postgresql, Database.mysql, Database.sqlite],
|
|
161
|
+
ORM.beanie: [Database.mongodb],
|
|
162
|
+
ORM.none: list(Database),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
MIGRATION_ORM_COMPAT: dict[str, list[str]] = {
|
|
166
|
+
MigrationTool.alembic: [ORM.sqlalchemy, ORM.sqlmodel],
|
|
167
|
+
MigrationTool.aerich: [ORM.tortoise],
|
|
168
|
+
MigrationTool.none: list(ORM),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# ── Human-readable labels ──────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
DB_LABELS = {
|
|
174
|
+
Database.postgresql: "PostgreSQL — production SQL",
|
|
175
|
+
Database.mysql: "MySQL / MariaDB",
|
|
176
|
+
Database.mongodb: "MongoDB",
|
|
177
|
+
Database.sqlite: "SQLite — local dev",
|
|
178
|
+
Database.supabase: "Supabase — managed Postgres + realtime",
|
|
179
|
+
Database.duckdb: "DuckDB — analytical / OLAP",
|
|
180
|
+
Database.none: "No database",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ORM_LABELS = {
|
|
184
|
+
ORM.sqlalchemy: "SQLAlchemy 2.x async",
|
|
185
|
+
ORM.sqlmodel: "SQLModel — Pydantic v2 + SQLAlchemy (type-safe)",
|
|
186
|
+
ORM.tortoise: "Tortoise ORM",
|
|
187
|
+
ORM.beanie: "Beanie — async MongoDB ODM",
|
|
188
|
+
ORM.none: "No ORM",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
AUTH_LABELS = {
|
|
192
|
+
AuthType.jwt: "JWT (python-jose + passlib)",
|
|
193
|
+
AuthType.oauth2: "OAuth2 Password flow",
|
|
194
|
+
AuthType.api_key: "API Key header",
|
|
195
|
+
AuthType.auth0: "Auth0 / Okta — managed identity",
|
|
196
|
+
AuthType.none: "No authentication",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
BROKER_LABELS = {
|
|
200
|
+
Broker.redis: "Redis — Celery broker + result backend",
|
|
201
|
+
Broker.rabbitmq: "RabbitMQ — Celery broker",
|
|
202
|
+
Broker.kafka: "Kafka — aiokafka streams",
|
|
203
|
+
Broker.arq: "Arq — lightweight async Redis task queue",
|
|
204
|
+
Broker.none: "No message broker",
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
STORAGE_LABELS = {
|
|
208
|
+
Storage.s3: "AWS S3 / MinIO (boto3)",
|
|
209
|
+
Storage.gcs: "Google Cloud Storage",
|
|
210
|
+
Storage.cloudinary: "Cloudinary — image/video CDN",
|
|
211
|
+
Storage.local: "Local filesystem",
|
|
212
|
+
Storage.none: "No file storage",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
AI_LABELS = {
|
|
216
|
+
AIProvider.openai: "OpenAI — GPT-4o, embeddings (custom base URL supported)",
|
|
217
|
+
AIProvider.anthropic: "Anthropic — Claude 3.x",
|
|
218
|
+
AIProvider.gemini: "Google Gemini — multi-modal",
|
|
219
|
+
AIProvider.ollama: "Ollama — local LLM, no API key",
|
|
220
|
+
AIProvider.langchain: "LangChain — LLM orchestration framework",
|
|
221
|
+
AIProvider.llamaindex: "LlamaIndex — RAG / data-to-LLM framework",
|
|
222
|
+
AIProvider.none: "No AI integration",
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
MONITORING_LABELS = {
|
|
226
|
+
MonitoringProvider.sentry: "Sentry — error tracking + performance",
|
|
227
|
+
MonitoringProvider.prometheus: "Prometheus — metrics via instrumentator",
|
|
228
|
+
MonitoringProvider.opentelemetry: "OpenTelemetry — vendor-neutral tracing",
|
|
229
|
+
MonitoringProvider.both: "Sentry + Prometheus",
|
|
230
|
+
MonitoringProvider.none: "No monitoring",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
EMAIL_LABELS = {
|
|
234
|
+
EmailProvider.sendgrid: "SendGrid",
|
|
235
|
+
EmailProvider.smtp: "SMTP (fastapi-mail)",
|
|
236
|
+
EmailProvider.ses: "AWS SES",
|
|
237
|
+
EmailProvider.none: "No email",
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
NOTIFICATION_LABELS = {
|
|
241
|
+
NotificationProvider.slack: "Slack — incoming webhooks",
|
|
242
|
+
NotificationProvider.discord: "Discord — webhooks",
|
|
243
|
+
NotificationProvider.none: "No notifications",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
LOG_DEST_LABELS = {
|
|
247
|
+
LogDestination.local: "Local — rotating file (daily, configurable retention)",
|
|
248
|
+
LogDestination.cloudwatch: "AWS CloudWatch Logs",
|
|
249
|
+
LogDestination.datadog: "Datadog Logs",
|
|
250
|
+
LogDestination.none: "Console only",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
VECTOR_DB_LABELS = {
|
|
254
|
+
VectorDB.qdrant: "Qdrant — local Docker or Qdrant Cloud",
|
|
255
|
+
VectorDB.chroma: "ChromaDB — local open-source vector DB",
|
|
256
|
+
VectorDB.pinecone: "Pinecone — managed cloud",
|
|
257
|
+
VectorDB.supabase: "Supabase pgvector",
|
|
258
|
+
VectorDB.elasticsearch: "Elasticsearch — kNN search",
|
|
259
|
+
VectorDB.none: "No vector database",
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
STACK_DESCRIPTIONS = {
|
|
263
|
+
Stack.minimal: "Core app only — no Docker, no infra",
|
|
264
|
+
Stack.standard: "App + Docker + GitHub CI",
|
|
265
|
+
Stack.full: "App + Docker + CI + Helm + Terraform",
|
|
266
|
+
}
|
|
@@ -176,6 +176,11 @@ class ProjectGenerator:
|
|
|
176
176
|
self._render_to(v1 / "health.py", "app/api/v1/health.py.j2")
|
|
177
177
|
if self.config.has_auth:
|
|
178
178
|
self._render_to(v1 / "auth.py", "app/api/v1/auth.py.j2")
|
|
179
|
+
if self.config.has_websockets:
|
|
180
|
+
self._render_to(v1 / "ws.py", "app/api/v1/ws.py.j2")
|
|
181
|
+
self._render_to(core / "ws_manager.py", "app/core/ws_manager.py.j2")
|
|
182
|
+
if self.config.has_graphql:
|
|
183
|
+
self._render_to(api / "graphql.py", "app/api/graphql.py.j2")
|
|
179
184
|
|
|
180
185
|
# db/ (only when a real database is chosen)
|
|
181
186
|
if self.config.db.value != "none":
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Strawberry GraphQL schema and router for {{ project_name }}."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import strawberry
|
|
8
|
+
from strawberry.fastapi import GraphQLRouter
|
|
9
|
+
from strawberry.scalars import JSON
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ── Types ──────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
@strawberry.type
|
|
15
|
+
class HealthType:
|
|
16
|
+
status: str
|
|
17
|
+
service: str
|
|
18
|
+
version: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@strawberry.type
|
|
22
|
+
class MessageType:
|
|
23
|
+
id: str
|
|
24
|
+
content: str
|
|
25
|
+
author: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@strawberry.input
|
|
29
|
+
class CreateMessageInput:
|
|
30
|
+
content: str
|
|
31
|
+
author: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── Query ──────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
@strawberry.type
|
|
37
|
+
class Query:
|
|
38
|
+
@strawberry.field(description="Health check for GraphQL layer")
|
|
39
|
+
def health(self) -> HealthType:
|
|
40
|
+
return HealthType(status="ok", service="{{ project_name }}", version="0.1.0")
|
|
41
|
+
|
|
42
|
+
@strawberry.field(description="List messages (stub — wire to DB)")
|
|
43
|
+
def messages(self, limit: int = 10) -> list[MessageType]:
|
|
44
|
+
# TODO: Replace with real DB query
|
|
45
|
+
return [
|
|
46
|
+
MessageType(id="1", content="Hello from GraphQL!", author="system"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
@strawberry.field(description="Get a message by ID (stub)")
|
|
50
|
+
def message(self, id: str) -> Optional[MessageType]:
|
|
51
|
+
# TODO: Replace with real DB query
|
|
52
|
+
if id == "1":
|
|
53
|
+
return MessageType(id="1", content="Hello from GraphQL!", author="system")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Mutation ───────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
@strawberry.type
|
|
60
|
+
class Mutation:
|
|
61
|
+
@strawberry.mutation(description="Create a new message (stub — wire to DB)")
|
|
62
|
+
def create_message(self, input: CreateMessageInput) -> MessageType:
|
|
63
|
+
import uuid
|
|
64
|
+
# TODO: Replace with real DB insert
|
|
65
|
+
return MessageType(id=str(uuid.uuid4()), content=input.content, author=input.author)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ── Subscription ───────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
import asyncio
|
|
71
|
+
from typing import AsyncGenerator
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@strawberry.type
|
|
75
|
+
class Subscription:
|
|
76
|
+
@strawberry.subscription(description="Subscribe to new messages (requires Redis pub/sub in production)")
|
|
77
|
+
async def message_added(self) -> AsyncGenerator[MessageType, None]:
|
|
78
|
+
"""Stub subscription — fires a fake message every 3 seconds."""
|
|
79
|
+
import uuid
|
|
80
|
+
while True:
|
|
81
|
+
await asyncio.sleep(3)
|
|
82
|
+
yield MessageType(id=str(uuid.uuid4()), content="Live update!", author="system")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── Schema & Router ────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
schema = strawberry.Schema(
|
|
88
|
+
query=Query,
|
|
89
|
+
mutation=Mutation,
|
|
90
|
+
subscription=Subscription,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Mount with: app.include_router(graphql_router, prefix="/graphql")
|
|
94
|
+
graphql_router = GraphQLRouter(
|
|
95
|
+
schema,
|
|
96
|
+
graphiql=True, # GraphiQL IDE at /graphql — disable in production
|
|
97
|
+
subscription_protocols=["graphql-transport-ws", "graphql-ws"],
|
|
98
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""WebSocket endpoints for {{ project_name }}."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status
|
|
6
|
+
from app.core.ws_manager import manager
|
|
7
|
+
from app.core.logger import logger
|
|
8
|
+
|
|
9
|
+
router = APIRouter(prefix="/ws")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.websocket("/connect")
|
|
13
|
+
async def ws_connect(websocket: WebSocket) -> None:
|
|
14
|
+
"""
|
|
15
|
+
General-purpose WebSocket endpoint.
|
|
16
|
+
Connect: ws://host/api/v1/ws/connect
|
|
17
|
+
|
|
18
|
+
Protocol:
|
|
19
|
+
Client → Server: {"type": "message", "data": "hello"}
|
|
20
|
+
Server → Client: {"type": "ack", "client_id": "...", "data": "..."}
|
|
21
|
+
"""
|
|
22
|
+
client_id = await manager.connect(websocket)
|
|
23
|
+
logger.info("WS connected: %s total=%d", client_id, manager.connected_count)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
await manager.send_personal(
|
|
27
|
+
{"type": "connected", "client_id": client_id, "total": manager.connected_count},
|
|
28
|
+
client_id,
|
|
29
|
+
)
|
|
30
|
+
while True:
|
|
31
|
+
data = await websocket.receive_json()
|
|
32
|
+
msg_type = data.get("type", "message")
|
|
33
|
+
|
|
34
|
+
if msg_type == "broadcast":
|
|
35
|
+
# Relay to all clients
|
|
36
|
+
await manager.broadcast({"type": "broadcast", "from": client_id, "data": data.get("data")})
|
|
37
|
+
elif msg_type == "ping":
|
|
38
|
+
await manager.send_personal({"type": "pong"}, client_id)
|
|
39
|
+
else:
|
|
40
|
+
# Echo back with ack
|
|
41
|
+
await manager.send_personal(
|
|
42
|
+
{"type": "ack", "client_id": client_id, "received": data}, client_id
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
except WebSocketDisconnect:
|
|
46
|
+
manager.disconnect(websocket)
|
|
47
|
+
logger.info("WS disconnected: %s total=%d", client_id, manager.connected_count)
|
|
48
|
+
await manager.broadcast({"type": "user_left", "client_id": client_id})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.websocket("/connect/{room_id}")
|
|
52
|
+
async def ws_room(websocket: WebSocket, room_id: str) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Room-scoped WebSocket endpoint.
|
|
55
|
+
Connect: ws://host/api/v1/ws/connect/{room_id}
|
|
56
|
+
"""
|
|
57
|
+
client_id = await manager.connect(websocket, client_id=f"{room_id}:{websocket.headers.get('sec-websocket-key', 'anon')[:8]}")
|
|
58
|
+
logger.info("WS room=%s client=%s", room_id, client_id)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
await manager.send_personal({"type": "joined", "room": room_id, "client_id": client_id}, client_id)
|
|
62
|
+
while True:
|
|
63
|
+
data = await websocket.receive_json()
|
|
64
|
+
await manager.broadcast({"type": "room_message", "room": room_id, "from": client_id, "data": data.get("data")})
|
|
65
|
+
except WebSocketDisconnect:
|
|
66
|
+
manager.disconnect(websocket)
|
|
67
|
+
await manager.broadcast({"type": "user_left", "room": room_id, "client_id": client_id})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""AI / LLM client utilities for {{ project_name }}."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
from app.core.config import settings
|
|
8
|
+
|
|
9
|
+
{% if ai == "openai" %}
|
|
10
|
+
from openai import AsyncOpenAI
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@lru_cache
|
|
14
|
+
def get_openai_client() -> AsyncOpenAI:
|
|
15
|
+
return AsyncOpenAI(
|
|
16
|
+
api_key=settings.OPENAI_API_KEY,
|
|
17
|
+
base_url=settings.OPENAI_BASE_URL or None,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def chat_completion(messages: list[dict], model: str | None = None, temperature: float = 0.7, max_tokens: int = 1024) -> str:
|
|
22
|
+
response = await get_openai_client().chat.completions.create(
|
|
23
|
+
model=model or settings.OPENAI_MODEL, messages=messages,
|
|
24
|
+
temperature=temperature, max_tokens=max_tokens,
|
|
25
|
+
)
|
|
26
|
+
return response.choices[0].message.content or ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def get_embedding(text: str, model: str | None = None) -> list[float]:
|
|
30
|
+
response = await get_openai_client().embeddings.create(
|
|
31
|
+
model=model or settings.OPENAI_EMBEDDING_MODEL, input=text,
|
|
32
|
+
)
|
|
33
|
+
return response.data[0].embedding
|
|
34
|
+
|
|
35
|
+
{% elif ai == "anthropic" %}
|
|
36
|
+
import anthropic
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@lru_cache
|
|
40
|
+
def get_anthropic_client() -> anthropic.AsyncAnthropic:
|
|
41
|
+
return anthropic.AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def chat_completion(messages: list[dict], system: str = "You are a helpful assistant.", model: str | None = None, max_tokens: int = 1024) -> str:
|
|
45
|
+
response = await get_anthropic_client().messages.create(
|
|
46
|
+
model=model or settings.ANTHROPIC_MODEL, max_tokens=max_tokens,
|
|
47
|
+
system=system, messages=messages,
|
|
48
|
+
)
|
|
49
|
+
return response.content[0].text
|
|
50
|
+
|
|
51
|
+
{% elif ai == "gemini" %}
|
|
52
|
+
import google.generativeai as genai
|
|
53
|
+
|
|
54
|
+
genai.configure(api_key=settings.GEMINI_API_KEY)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def chat_completion(prompt: str, model: str | None = None) -> str:
|
|
58
|
+
m = genai.GenerativeModel(model or settings.GEMINI_MODEL)
|
|
59
|
+
response = m.generate_content(prompt)
|
|
60
|
+
return response.text
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def get_embedding(text: str) -> list[float]:
|
|
64
|
+
result = genai.embed_content(model="models/text-embedding-004", content=text)
|
|
65
|
+
return result["embedding"]
|
|
66
|
+
|
|
67
|
+
{% elif ai == "ollama" %}
|
|
68
|
+
import httpx
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def chat_completion(messages: list[dict], model: str | None = None, temperature: float = 0.7) -> str:
|
|
72
|
+
async with httpx.AsyncClient(timeout=120) as client:
|
|
73
|
+
r = await client.post(
|
|
74
|
+
f"{settings.ollama_url}/api/chat",
|
|
75
|
+
json={"model": model or settings.OLLAMA_MODEL, "messages": messages,
|
|
76
|
+
"stream": False, "options": {"temperature": temperature}},
|
|
77
|
+
)
|
|
78
|
+
r.raise_for_status()
|
|
79
|
+
return r.json()["message"]["content"]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def get_embedding(text: str, model: str = "nomic-embed-text") -> list[float]:
|
|
83
|
+
async with httpx.AsyncClient(timeout=60) as client:
|
|
84
|
+
r = await client.post(f"{settings.ollama_url}/api/embeddings", json={"model": model, "prompt": text})
|
|
85
|
+
r.raise_for_status()
|
|
86
|
+
return r.json()["embedding"]
|
|
87
|
+
|
|
88
|
+
{% elif ai == "langchain" %}
|
|
89
|
+
from langchain_openai import ChatOpenAI
|
|
90
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@lru_cache
|
|
94
|
+
def get_llm() -> ChatOpenAI:
|
|
95
|
+
"""Return a cached LangChain ChatOpenAI instance."""
|
|
96
|
+
return ChatOpenAI(
|
|
97
|
+
api_key=settings.OPENAI_API_KEY,
|
|
98
|
+
model=settings.OPENAI_MODEL,
|
|
99
|
+
base_url=settings.OPENAI_BASE_URL or None,
|
|
100
|
+
temperature=0.7,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def chat_completion(user_message: str, system: str = "You are a helpful assistant.") -> str:
|
|
105
|
+
"""Single-turn chat completion via LangChain."""
|
|
106
|
+
llm = get_llm()
|
|
107
|
+
messages = [SystemMessage(content=system), HumanMessage(content=user_message)]
|
|
108
|
+
response = await llm.ainvoke(messages)
|
|
109
|
+
return response.content
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def get_embedding(text: str) -> list[float]:
|
|
113
|
+
"""Generate an embedding using LangChain OpenAI embeddings."""
|
|
114
|
+
from langchain_openai import OpenAIEmbeddings
|
|
115
|
+
embeddings = OpenAIEmbeddings(api_key=settings.OPENAI_API_KEY, model=settings.OPENAI_EMBEDDING_MODEL)
|
|
116
|
+
return await embeddings.aembed_query(text)
|
|
117
|
+
|
|
118
|
+
{% elif ai == "llamaindex" %}
|
|
119
|
+
from llama_index.core import Settings as LlamaSettings
|
|
120
|
+
from llama_index.llms.openai import OpenAI
|
|
121
|
+
from llama_index.embeddings.openai import OpenAIEmbedding
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def configure_llama_index() -> None:
|
|
125
|
+
"""Configure LlamaIndex global settings. Call once at startup."""
|
|
126
|
+
LlamaSettings.llm = OpenAI(
|
|
127
|
+
api_key=settings.OPENAI_API_KEY,
|
|
128
|
+
model=settings.OPENAI_MODEL,
|
|
129
|
+
api_base=settings.OPENAI_BASE_URL or None,
|
|
130
|
+
)
|
|
131
|
+
LlamaSettings.embed_model = OpenAIEmbedding(
|
|
132
|
+
api_key=settings.OPENAI_API_KEY,
|
|
133
|
+
model=settings.OPENAI_EMBEDDING_MODEL,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def query_index(index, question: str, similarity_top_k: int = 5) -> str:
|
|
138
|
+
"""Query a LlamaIndex VectorStoreIndex."""
|
|
139
|
+
query_engine = index.as_query_engine(similarity_top_k=similarity_top_k)
|
|
140
|
+
response = await query_engine.aquery(question)
|
|
141
|
+
return str(response)
|
|
142
|
+
|
|
143
|
+
{% endif %}
|