fastapi-spawn 0.4.6__tar.gz → 0.4.9__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.4.6 → fastapi_spawn-0.4.9}/PKG-INFO +1 -1
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/__init__.py +1 -1
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/cli.py +2 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/config.py +3 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/constants.py +2 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/generator.py +26 -0
- fastapi_spawn-0.4.9/fastapi_spawn/templates/app/api/v1/auth/sso.py.j2 +71 -0
- fastapi_spawn-0.4.9/fastapi_spawn/templates/app/api/v1/payments/router.py.j2 +58 -0
- fastapi_spawn-0.4.9/fastapi_spawn/templates/app/api/v1/streaming/router.py.j2 +26 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/email.py.j2 +23 -0
- fastapi_spawn-0.4.9/fastapi_spawn/templates/app/core/ocr.py.j2 +40 -0
- fastapi_spawn-0.4.9/fastapi_spawn/templates/app/core/search.py.j2 +23 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/base/pyproject.toml.j2 +24 -0
- fastapi_spawn-0.4.9/fastapi_spawn/templates/db/seed.py.j2 +61 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/pyproject.toml +1 -1
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/.gitignore +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/LICENSE +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/README.md +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/interactive.py +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/alembic/alembic.ini.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/alembic/env.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/__init__.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/api/deps.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/api/graphql.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/api/v1/auth/router.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/api/v1/health/router.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/api/v1/router.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/api/v1/ws/router.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/ai.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/config.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/exceptions.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/logger.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/logging.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/monitoring.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/notifications.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/security.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/storage.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/vector_db.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/ws_manager.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/db/session.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/frontend/index.html.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/main.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/middleware/__init__.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/middleware/rate_limit.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/middleware/request_logger.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/base/Makefile.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/base/README.md.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/base/env.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/base/env_example.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/base/gitignore.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/base/pre_commit.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/ci/github/publish.yml.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/ci/github/tests.yml.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/ci/gitlab/gitlab-ci.yml.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/docker/Dockerfile.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/docker/docker-compose.yml.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/docker/dockerignore.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/infra/docker/docker-compose.prod.yml.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/infra/helm/Chart.yaml.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/infra/helm/values.yaml.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/infra/terraform/main.tf.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/infra/terraform/variables.tf.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/root/main.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/tasks/arq_worker.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/tasks/celery_app.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/tasks/sample_tasks.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/tests/conftest.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/tests/test_health.py.j2 +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/utils.py +0 -0
- {fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-spawn
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.9
|
|
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
|
|
@@ -98,6 +98,7 @@ def new(
|
|
|
98
98
|
no_tests: bool = typer.Option(False, "--no-tests", help="Skip test suite"),
|
|
99
99
|
dry_run: bool = typer.Option(False, "--dry-run", help="Preview structure without writing files"),
|
|
100
100
|
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing directory"),
|
|
101
|
+
extra: Optional[list[str]] = typer.Option(None, "--extra", help="Extra integrations (e.g. stripe, sso, sse, seed, ocr, meilisearch)"),
|
|
101
102
|
output: Path = typer.Option(Path("."), "--output", "-o", help="Output directory"),
|
|
102
103
|
version: Optional[bool] = typer.Option(
|
|
103
104
|
None, "--version", "-v", callback=version_callback, is_eager=True, help="Show version"
|
|
@@ -192,6 +193,7 @@ def new(
|
|
|
192
193
|
include_tests=include_tests,
|
|
193
194
|
dry_run=dry_run,
|
|
194
195
|
force=force,
|
|
196
|
+
extras=extra or [],
|
|
195
197
|
)
|
|
196
198
|
|
|
197
199
|
_print_summary(config)
|
|
@@ -47,6 +47,7 @@ class ProjectConfig:
|
|
|
47
47
|
include_makefile: bool = True
|
|
48
48
|
dry_run: bool = False
|
|
49
49
|
force: bool = False
|
|
50
|
+
extras: list[str] = field(default_factory=list)
|
|
50
51
|
# Derived (post-init)
|
|
51
52
|
package_name: str = field(default="", init=False)
|
|
52
53
|
slug: str = field(default="", init=False)
|
|
@@ -194,6 +195,7 @@ class ProjectConfig:
|
|
|
194
195
|
"has_log_file": self.has_log_file,
|
|
195
196
|
"include_tests": self.include_tests,
|
|
196
197
|
"include_makefile": self.include_makefile,
|
|
198
|
+
"extras": self.extras,
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
def summary_lines(self) -> list[tuple[str, str]]:
|
|
@@ -219,5 +221,6 @@ class ProjectConfig:
|
|
|
219
221
|
("API extras", self.api_extra.value),
|
|
220
222
|
("Docker", "yes" if self.has_docker else "no"),
|
|
221
223
|
("Tests", "yes" if self.include_tests else "no"),
|
|
224
|
+
("Extras", ", ".join(self.extras) if self.extras else "none"),
|
|
222
225
|
("Dry-run", "yes" if self.dry_run else "no"),
|
|
223
226
|
]
|
|
@@ -93,6 +93,7 @@ class EmailProvider(str, Enum):
|
|
|
93
93
|
sendgrid = "sendgrid"
|
|
94
94
|
smtp = "smtp"
|
|
95
95
|
ses = "ses"
|
|
96
|
+
resend = "resend"
|
|
96
97
|
none = "none"
|
|
97
98
|
|
|
98
99
|
|
|
@@ -234,6 +235,7 @@ EMAIL_LABELS = {
|
|
|
234
235
|
EmailProvider.sendgrid: "SendGrid",
|
|
235
236
|
EmailProvider.smtp: "SMTP (fastapi-mail)",
|
|
236
237
|
EmailProvider.ses: "AWS SES",
|
|
238
|
+
EmailProvider.resend: "Resend",
|
|
237
239
|
EmailProvider.none: "No email",
|
|
238
240
|
}
|
|
239
241
|
|
|
@@ -209,6 +209,32 @@ class ProjectGenerator:
|
|
|
209
209
|
(root / "logs").mkdir(exist_ok=True)
|
|
210
210
|
(root / "logs" / ".gitkeep").write_text("", encoding="utf-8")
|
|
211
211
|
|
|
212
|
+
# Extras rendering
|
|
213
|
+
extras = self.config.extras
|
|
214
|
+
if "stripe" in extras:
|
|
215
|
+
(v1 / "payments").mkdir(parents=True, exist_ok=True)
|
|
216
|
+
self._render_to(v1 / "payments" / "router.py", "app/api/v1/payments/router.py.j2")
|
|
217
|
+
self._render_to(v1 / "payments" / "__init__.py", "app/__init__.py.j2")
|
|
218
|
+
|
|
219
|
+
if "sso" in extras:
|
|
220
|
+
(v1 / "auth").mkdir(parents=True, exist_ok=True)
|
|
221
|
+
self._render_to(v1 / "auth" / "sso.py", "app/api/v1/auth/sso.py.j2")
|
|
222
|
+
|
|
223
|
+
if "sse" in extras:
|
|
224
|
+
(v1 / "streaming").mkdir(parents=True, exist_ok=True)
|
|
225
|
+
self._render_to(v1 / "streaming" / "router.py", "app/api/v1/streaming/router.py.j2")
|
|
226
|
+
self._render_to(v1 / "streaming" / "__init__.py", "app/__init__.py.j2")
|
|
227
|
+
|
|
228
|
+
if "seed" in extras:
|
|
229
|
+
(root / "db").mkdir(parents=True, exist_ok=True)
|
|
230
|
+
self._render_to(root / "db" / "seed.py", "db/seed.py.j2")
|
|
231
|
+
|
|
232
|
+
if "ocr" in extras:
|
|
233
|
+
self._render_to(core / "ocr.py", "app/core/ocr.py.j2")
|
|
234
|
+
|
|
235
|
+
if "meilisearch" in extras:
|
|
236
|
+
self._render_to(core / "search.py", "app/core/search.py.j2")
|
|
237
|
+
|
|
212
238
|
|
|
213
239
|
def _generate_tasks(self, root: Path) -> None:
|
|
214
240
|
"""Root-level tasks/ directory."""
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request, HTTPException
|
|
2
|
+
from fastapi_sso.sso.google import GoogleSSO
|
|
3
|
+
from fastapi_sso.sso.github import GithubSSO
|
|
4
|
+
from app.core.config import settings
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix="/sso", tags=["auth"])
|
|
7
|
+
|
|
8
|
+
# Requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in .env
|
|
9
|
+
google_sso = GoogleSSO(
|
|
10
|
+
client_id=getattr(settings, "GOOGLE_CLIENT_ID", "placeholder"),
|
|
11
|
+
client_secret=getattr(settings, "GOOGLE_CLIENT_SECRET", "placeholder"),
|
|
12
|
+
redirect_uri="http://localhost:8000/api/v1/auth/sso/google/callback",
|
|
13
|
+
allow_insecure_http=True, # Set to False in production
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
@router.get("/google/login")
|
|
17
|
+
async def google_login():
|
|
18
|
+
"""Redirects the user to the Google login page."""
|
|
19
|
+
with google_sso:
|
|
20
|
+
return await google_sso.get_login_redirect()
|
|
21
|
+
|
|
22
|
+
@router.get("/google/callback")
|
|
23
|
+
async def google_callback(request: Request):
|
|
24
|
+
"""Process login response from Google and return user info."""
|
|
25
|
+
with google_sso:
|
|
26
|
+
user = await google_sso.verify_and_process(request)
|
|
27
|
+
if not user:
|
|
28
|
+
raise HTTPException(status_code=400, detail="Failed to login via Google")
|
|
29
|
+
|
|
30
|
+
# TODO: Create or update user in database, then generate and return JWT
|
|
31
|
+
return {
|
|
32
|
+
"id": user.id,
|
|
33
|
+
"email": user.email,
|
|
34
|
+
"first_name": user.first_name,
|
|
35
|
+
"last_name": user.last_name,
|
|
36
|
+
"picture": user.picture,
|
|
37
|
+
"provider": "google"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# --- GitHub SSO ---
|
|
41
|
+
|
|
42
|
+
# Requires GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET in .env
|
|
43
|
+
github_sso = GithubSSO(
|
|
44
|
+
client_id=getattr(settings, "GITHUB_CLIENT_ID", "placeholder"),
|
|
45
|
+
client_secret=getattr(settings, "GITHUB_CLIENT_SECRET", "placeholder"),
|
|
46
|
+
redirect_uri="http://localhost:8000/api/v1/auth/sso/github/callback",
|
|
47
|
+
allow_insecure_http=True, # Set to False in production
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@router.get("/github/login")
|
|
51
|
+
async def github_login():
|
|
52
|
+
"""Redirects the user to the GitHub login page."""
|
|
53
|
+
with github_sso:
|
|
54
|
+
return await github_sso.get_login_redirect()
|
|
55
|
+
|
|
56
|
+
@router.get("/github/callback")
|
|
57
|
+
async def github_callback(request: Request):
|
|
58
|
+
"""Process login response from GitHub and return user info."""
|
|
59
|
+
with github_sso:
|
|
60
|
+
user = await github_sso.verify_and_process(request)
|
|
61
|
+
if not user:
|
|
62
|
+
raise HTTPException(status_code=400, detail="Failed to login via GitHub")
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"id": user.id,
|
|
66
|
+
"email": user.email,
|
|
67
|
+
"first_name": getattr(user, "first_name", None),
|
|
68
|
+
"last_name": getattr(user, "last_name", None),
|
|
69
|
+
"picture": getattr(user, "picture", None),
|
|
70
|
+
"provider": "github"
|
|
71
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request, HTTPException, Depends
|
|
2
|
+
import stripe
|
|
3
|
+
from app.core.config import settings
|
|
4
|
+
from app.core.logger import logger
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix="/payments", tags=["payments"])
|
|
7
|
+
|
|
8
|
+
stripe.api_key = getattr(settings, "STRIPE_API_KEY", "sk_test_placeholder")
|
|
9
|
+
webhook_secret = getattr(settings, "STRIPE_WEBHOOK_SECRET", "whsec_placeholder")
|
|
10
|
+
|
|
11
|
+
@router.post("/webhook")
|
|
12
|
+
async def stripe_webhook(request: Request):
|
|
13
|
+
payload = await request.body()
|
|
14
|
+
sig_header = request.headers.get("stripe-signature")
|
|
15
|
+
|
|
16
|
+
if not sig_header:
|
|
17
|
+
raise HTTPException(status_code=400, detail="Missing signature")
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
event = stripe.Webhook.construct_event(
|
|
21
|
+
payload, sig_header, webhook_secret
|
|
22
|
+
)
|
|
23
|
+
except ValueError as e:
|
|
24
|
+
logger.error(f"Invalid payload: {e}")
|
|
25
|
+
raise HTTPException(status_code=400, detail="Invalid payload")
|
|
26
|
+
except stripe.error.SignatureVerificationError as e:
|
|
27
|
+
logger.error(f"Invalid signature: {e}")
|
|
28
|
+
raise HTTPException(status_code=400, detail="Invalid signature")
|
|
29
|
+
|
|
30
|
+
# Handle the event
|
|
31
|
+
if event.type == "checkout.session.completed":
|
|
32
|
+
session = event.data.object
|
|
33
|
+
logger.info(f"Payment successful for session {session.id}")
|
|
34
|
+
# TODO: Fulfill the purchase...
|
|
35
|
+
|
|
36
|
+
return {"status": "success"}
|
|
37
|
+
|
|
38
|
+
@router.post("/create-checkout-session")
|
|
39
|
+
async def create_checkout_session():
|
|
40
|
+
try:
|
|
41
|
+
checkout_session = stripe.checkout.Session.create(
|
|
42
|
+
line_items=[
|
|
43
|
+
{
|
|
44
|
+
"price_data": {
|
|
45
|
+
"currency": "usd",
|
|
46
|
+
"product_data": {"name": "Pro Subscription"},
|
|
47
|
+
"unit_amount": 2000,
|
|
48
|
+
},
|
|
49
|
+
"quantity": 1,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
mode="payment",
|
|
53
|
+
success_url="http://localhost:3000/success",
|
|
54
|
+
cancel_url="http://localhost:3000/cancel",
|
|
55
|
+
)
|
|
56
|
+
return {"url": checkout_session.url}
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request, HTTPException
|
|
2
|
+
from sse_starlette.sse import EventSourceResponse
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
router = APIRouter(prefix="/streaming", tags=["streaming"])
|
|
6
|
+
|
|
7
|
+
async def message_generator():
|
|
8
|
+
"""Simulates a stream of LLM tokens."""
|
|
9
|
+
tokens = ["Hello", " ", "there", "!", " ", "This", " ", "is", " ", "a", " ", "stream", "."]
|
|
10
|
+
for token in tokens:
|
|
11
|
+
await asyncio.sleep(0.1) # Simulate processing delay
|
|
12
|
+
yield {
|
|
13
|
+
"event": "message",
|
|
14
|
+
"id": "message_id",
|
|
15
|
+
"retry": 15000,
|
|
16
|
+
"data": token
|
|
17
|
+
}
|
|
18
|
+
yield {"event": "done", "data": "[DONE]"}
|
|
19
|
+
|
|
20
|
+
@router.get("/chat")
|
|
21
|
+
async def stream_chat(request: Request):
|
|
22
|
+
"""
|
|
23
|
+
Server-Sent Events endpoint for streaming chat responses.
|
|
24
|
+
Client can consume this using EventSource API in JS.
|
|
25
|
+
"""
|
|
26
|
+
return EventSourceResponse(message_generator())
|
|
@@ -84,4 +84,27 @@ async def send_email(
|
|
|
84
84
|
except ClientError:
|
|
85
85
|
return False
|
|
86
86
|
|
|
87
|
+
{% elif email == "resend" %}
|
|
88
|
+
import resend
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def send_email(
|
|
92
|
+
to: str,
|
|
93
|
+
subject: str,
|
|
94
|
+
html_content: str,
|
|
95
|
+
from_email: str | None = None,
|
|
96
|
+
) -> bool:
|
|
97
|
+
"""Send an email via Resend. Returns True on success."""
|
|
98
|
+
resend.api_key = settings.RESEND_API_KEY
|
|
99
|
+
try:
|
|
100
|
+
resend.Emails.send({
|
|
101
|
+
"from": from_email or settings.RESEND_FROM_EMAIL,
|
|
102
|
+
"to": to,
|
|
103
|
+
"subject": subject,
|
|
104
|
+
"html": html_content
|
|
105
|
+
})
|
|
106
|
+
return True
|
|
107
|
+
except Exception:
|
|
108
|
+
return False
|
|
109
|
+
|
|
87
110
|
{% endif %}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from typing import Optional
|
|
3
|
+
try:
|
|
4
|
+
import fitz # PyMuPDF
|
|
5
|
+
except ImportError:
|
|
6
|
+
fitz = None
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import pytesseract
|
|
10
|
+
from PIL import Image
|
|
11
|
+
except ImportError:
|
|
12
|
+
pytesseract = None
|
|
13
|
+
|
|
14
|
+
class OCRService:
|
|
15
|
+
"""
|
|
16
|
+
Handles PDF parsing and Optical Character Recognition.
|
|
17
|
+
Requires `pymupdf` and `pytesseract` to be installed.
|
|
18
|
+
System requirement: `sudo apt install tesseract-ocr`
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def extract_text_from_pdf(pdf_bytes: bytes) -> str:
|
|
23
|
+
"""Extract text from a native PDF."""
|
|
24
|
+
if fitz is None:
|
|
25
|
+
raise RuntimeError("PyMuPDF is not installed. Run: pip install pymupdf")
|
|
26
|
+
|
|
27
|
+
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
|
28
|
+
text_chunks = []
|
|
29
|
+
for page in doc:
|
|
30
|
+
text_chunks.append(page.get_text())
|
|
31
|
+
return "\n".join(text_chunks)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def extract_text_from_image(image_bytes: bytes) -> str:
|
|
35
|
+
"""Perform OCR on an image."""
|
|
36
|
+
if pytesseract is None:
|
|
37
|
+
raise RuntimeError("pytesseract is not installed. Run: pip install pytesseract pillow")
|
|
38
|
+
|
|
39
|
+
img = Image.open(io.BytesIO(image_bytes))
|
|
40
|
+
return pytesseract.image_to_string(img)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import meilisearch
|
|
2
|
+
from app.core.config import settings
|
|
3
|
+
|
|
4
|
+
class MeilisearchService:
|
|
5
|
+
"""
|
|
6
|
+
Client for Meilisearch — an open-source, lightning-fast, and hyper-relevant search engine.
|
|
7
|
+
"""
|
|
8
|
+
def __init__(self):
|
|
9
|
+
host = getattr(settings, "MEILISEARCH_HOST", "http://localhost:7700")
|
|
10
|
+
api_key = getattr(settings, "MEILISEARCH_API_KEY", "masterKey")
|
|
11
|
+
self.client = meilisearch.Client(host, api_key)
|
|
12
|
+
|
|
13
|
+
def add_documents(self, index_name: str, documents: list[dict]):
|
|
14
|
+
"""Add or update documents in an index."""
|
|
15
|
+
index = self.client.index(index_name)
|
|
16
|
+
return index.add_documents(documents)
|
|
17
|
+
|
|
18
|
+
def search(self, index_name: str, query: str, limit: int = 20):
|
|
19
|
+
"""Perform a typo-tolerant search."""
|
|
20
|
+
index = self.client.index(index_name)
|
|
21
|
+
return index.search(query, {"limit": limit})
|
|
22
|
+
|
|
23
|
+
search_client = MeilisearchService()
|
|
@@ -93,6 +93,8 @@ dependencies = [
|
|
|
93
93
|
{% elif email == "ses" %}
|
|
94
94
|
# Uses boto3 (already included above if has_s3, else add it)
|
|
95
95
|
"boto3>=1.34.0",
|
|
96
|
+
{% elif email == "resend" %}
|
|
97
|
+
"resend>=2.1.0",
|
|
96
98
|
{% endif %}
|
|
97
99
|
|
|
98
100
|
{% if has_notify %}
|
|
@@ -150,6 +152,28 @@ dependencies = [
|
|
|
150
152
|
{% elif log_dest == "datadog" %}
|
|
151
153
|
"datadog-lambda>=6.0.0",
|
|
152
154
|
{% endif %}
|
|
155
|
+
|
|
156
|
+
{% if extras %}
|
|
157
|
+
{% if "stripe" in extras %}
|
|
158
|
+
"stripe>=9.0.0",
|
|
159
|
+
{% endif %}
|
|
160
|
+
{% if "sso" in extras %}
|
|
161
|
+
"fastapi-sso>=0.14.0",
|
|
162
|
+
{% endif %}
|
|
163
|
+
{% if "sse" in extras %}
|
|
164
|
+
"sse-starlette>=2.1.0",
|
|
165
|
+
{% endif %}
|
|
166
|
+
{% if "seed" in extras %}
|
|
167
|
+
"faker>=25.0.0",
|
|
168
|
+
{% endif %}
|
|
169
|
+
{% if "ocr" in extras %}
|
|
170
|
+
"pymupdf>=1.24.0",
|
|
171
|
+
"pytesseract>=0.3.10",
|
|
172
|
+
{% endif %}
|
|
173
|
+
{% if "meilisearch" in extras %}
|
|
174
|
+
"meilisearch>=0.30.0",
|
|
175
|
+
{% endif %}
|
|
176
|
+
{% endif %}
|
|
153
177
|
]
|
|
154
178
|
|
|
155
179
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from faker import Faker
|
|
4
|
+
|
|
5
|
+
logging.basicConfig(level=logging.INFO)
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
fake = Faker()
|
|
8
|
+
|
|
9
|
+
async def seed_database():
|
|
10
|
+
"""
|
|
11
|
+
Seed the database with mock data.
|
|
12
|
+
Replace the stubs below with your actual database models and sessions.
|
|
13
|
+
"""
|
|
14
|
+
logger.info("Seeding database...")
|
|
15
|
+
|
|
16
|
+
# Example for SQLAlchemy:
|
|
17
|
+
# from app.db.session import async_session_maker
|
|
18
|
+
# from app.models.user import User
|
|
19
|
+
# from app.models.post import Post
|
|
20
|
+
# from app.models.comment import Comment
|
|
21
|
+
|
|
22
|
+
# async with async_session_maker() as session:
|
|
23
|
+
# users = []
|
|
24
|
+
# # 1. Create Users
|
|
25
|
+
# for _ in range(50):
|
|
26
|
+
# user = User(
|
|
27
|
+
# email=fake.email(),
|
|
28
|
+
# full_name=fake.name(),
|
|
29
|
+
# is_active=True
|
|
30
|
+
# )
|
|
31
|
+
# session.add(user)
|
|
32
|
+
# users.append(user)
|
|
33
|
+
# await session.commit()
|
|
34
|
+
#
|
|
35
|
+
# # 2. Create Posts
|
|
36
|
+
# posts = []
|
|
37
|
+
# for _ in range(200):
|
|
38
|
+
# post = Post(
|
|
39
|
+
# title=fake.sentence(),
|
|
40
|
+
# content=fake.text(),
|
|
41
|
+
# author_id=fake.random_element(elements=users).id
|
|
42
|
+
# )
|
|
43
|
+
# session.add(post)
|
|
44
|
+
# posts.append(post)
|
|
45
|
+
# await session.commit()
|
|
46
|
+
#
|
|
47
|
+
# # 3. Create Comments
|
|
48
|
+
# for _ in range(500):
|
|
49
|
+
# comment = Comment(
|
|
50
|
+
# text=fake.paragraph(),
|
|
51
|
+
# post_id=fake.random_element(elements=posts).id,
|
|
52
|
+
# user_id=fake.random_element(elements=users).id
|
|
53
|
+
# )
|
|
54
|
+
# session.add(comment)
|
|
55
|
+
# await session.commit()
|
|
56
|
+
|
|
57
|
+
logger.info("Generated 50 mock users, 200 posts, and 500 comments (Stub)")
|
|
58
|
+
logger.info("Database seeding complete!")
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
asyncio.run(seed_database())
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fastapi-spawn"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.9"
|
|
8
8
|
description = "A powerful CLI tool to scaffold production-ready FastAPI projects with flexible database, auth, broker, and deployment options."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/api/v1/auth/router.py.j2
RENAMED
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/api/v1/health/router.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/api/v1/ws/router.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/exceptions.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/monitoring.py.j2
RENAMED
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/notifications.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/vector_db.py.j2
RENAMED
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/core/ws_manager.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/frontend/index.html.j2
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/middleware/__init__.py.j2
RENAMED
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/app/middleware/rate_limit.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/ci/github/publish.yml.j2
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/ci/gitlab/gitlab-ci.yml.j2
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/docker/docker-compose.yml.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/infra/helm/Chart.yaml.j2
RENAMED
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/infra/helm/values.yaml.j2
RENAMED
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/infra/terraform/main.tf.j2
RENAMED
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/infra/terraform/variables.tf.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_spawn-0.4.6 → fastapi_spawn-0.4.9}/fastapi_spawn/templates/tasks/sample_tasks.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|