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.
Files changed (68) hide show
  1. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/PKG-INFO +1 -1
  2. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/__init__.py +1 -1
  3. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/config.py +15 -1
  4. fastapi_spawn-0.3.0/fastapi_spawn/constants.py +266 -0
  5. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/generator.py +5 -0
  6. fastapi_spawn-0.3.0/fastapi_spawn/templates/app/api/graphql.py.j2 +98 -0
  7. fastapi_spawn-0.3.0/fastapi_spawn/templates/app/api/v1/ws.py.j2 +67 -0
  8. fastapi_spawn-0.3.0/fastapi_spawn/templates/app/core/ai.py.j2 +143 -0
  9. fastapi_spawn-0.3.0/fastapi_spawn/templates/app/core/storage.py.j2 +121 -0
  10. fastapi_spawn-0.3.0/fastapi_spawn/templates/app/core/vector_db.py.j2 +118 -0
  11. fastapi_spawn-0.3.0/fastapi_spawn/templates/app/core/ws_manager.py.j2 +74 -0
  12. fastapi_spawn-0.3.0/fastapi_spawn/templates/base/env_example.j2 +300 -0
  13. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/pyproject.toml.j2 +37 -0
  14. fastapi_spawn-0.3.0/fastapi_spawn/templates/tasks/arq_worker.py.j2 +56 -0
  15. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/pyproject.toml +1 -1
  16. fastapi_spawn-0.2.0/fastapi_spawn/constants.py +0 -223
  17. fastapi_spawn-0.2.0/fastapi_spawn/templates/app/core/ai.py.j2 +0 -140
  18. fastapi_spawn-0.2.0/fastapi_spawn/templates/app/core/storage.py.j2 +0 -73
  19. fastapi_spawn-0.2.0/fastapi_spawn/templates/app/core/vector_db.py.j2 +0 -92
  20. fastapi_spawn-0.2.0/fastapi_spawn/templates/base/env_example.j2 +0 -243
  21. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/.gitignore +0 -0
  22. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/CHANGELOG.md +0 -0
  23. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/LICENSE +0 -0
  24. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/README.md +0 -0
  25. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/cli.py +0 -0
  26. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/interactive.py +0 -0
  27. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/alembic/alembic.ini.j2 +0 -0
  28. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/alembic/env.py.j2 +0 -0
  29. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/__init__.py.j2 +0 -0
  30. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/api/deps.py.j2 +0 -0
  31. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/api/v1/auth.py.j2 +0 -0
  32. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/api/v1/health.py.j2 +0 -0
  33. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/config.py.j2 +0 -0
  34. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/email.py.j2 +0 -0
  35. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/exceptions.py.j2 +0 -0
  36. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/logger.py.j2 +0 -0
  37. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/logging.py.j2 +0 -0
  38. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/monitoring.py.j2 +0 -0
  39. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/notifications.py.j2 +0 -0
  40. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/core/security.py.j2 +0 -0
  41. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/db/session.py.j2 +0 -0
  42. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/main.py.j2 +0 -0
  43. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/middleware/__init__.py.j2 +0 -0
  44. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/middleware/rate_limit.py.j2 +0 -0
  45. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/app/middleware/request_logger.py.j2 +0 -0
  46. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/Makefile.j2 +0 -0
  47. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/README.md.j2 +0 -0
  48. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/env.j2 +0 -0
  49. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/gitignore.j2 +0 -0
  50. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/base/pre_commit.j2 +0 -0
  51. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/ci/github/publish.yml.j2 +0 -0
  52. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/ci/github/tests.yml.j2 +0 -0
  53. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/ci/gitlab/gitlab-ci.yml.j2 +0 -0
  54. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/docker/Dockerfile.j2 +0 -0
  55. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/docker/docker-compose.yml.j2 +0 -0
  56. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/docker/dockerignore.j2 +0 -0
  57. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/docker/docker-compose.prod.yml.j2 +0 -0
  58. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/helm/Chart.yaml.j2 +0 -0
  59. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/helm/values.yaml.j2 +0 -0
  60. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/terraform/main.tf.j2 +0 -0
  61. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/infra/terraform/variables.tf.j2 +0 -0
  62. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/root/main.py.j2 +0 -0
  63. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/tasks/celery_app.py.j2 +0 -0
  64. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/tasks/sample_tasks.py.j2 +0 -0
  65. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/tests/conftest.py.j2 +0 -0
  66. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/templates/tests/test_health.py.j2 +0 -0
  67. {fastapi_spawn-0.2.0 → fastapi_spawn-0.3.0}/fastapi_spawn/utils.py +0 -0
  68. {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.2.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
@@ -1,6 +1,6 @@
1
1
  """fastapi-spawn — Production-ready FastAPI project scaffolding CLI."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
4
4
  __author__ = "Bishwajit Garai"
5
5
  __email__ = "bishwajitgarai@gmail.com"
6
6
  __license__ = "MIT"
@@ -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 %}