diff --git a/packages/template-generator/src/template-handlers/ecosystems/python-base.ts b/packages/template-generator/src/template-handlers/ecosystems/python-base.ts index 8bf5eede8..8b513f756 100644 --- a/packages/template-generator/src/template-handlers/ecosystems/python-base.ts +++ b/packages/template-generator/src/template-handlers/ecosystems/python-base.ts @@ -2,6 +2,7 @@ import type { ProjectConfig } from "@better-fullstack/types"; import type { VirtualFileSystem } from "@/core/virtual-fs"; +import { transformFilename } from "@/core/template-processor"; import { renderTemplateTree, type TemplateData } from "@/template-handlers/core/utils"; export async function processPythonBaseTemplate( @@ -22,9 +23,18 @@ export async function processPythonBaseTemplate( pythonLogging: config.pythonLogging ?? "none", }; + const appEntryDir = "src/app/main/"; + const appEntry = `${prefix}${appEntryDir}${config.pythonWebFramework ?? "none"}.py.hbs`; + const selectedAppEntry = templates.has(appEntry) + ? appEntry + : `${prefix}${appEntryDir}none.py.hbs`; + renderTemplateTree(vfs, templates, { prefix, targetPath, context: templateContext, + outputPath: (relativePath) => + relativePath.startsWith(appEntryDir) ? "src/app/main.py" : transformFilename(relativePath), + skip: (templatePath) => templatePath.includes(appEntryDir) && templatePath !== selectedAppEntry, }); } diff --git a/packages/template-generator/templates/python-base/src/app/main/aiohttp.py.hbs b/packages/template-generator/templates/python-base/src/app/main/aiohttp.py.hbs new file mode 100644 index 000000000..33415f9e8 --- /dev/null +++ b/packages/template-generator/templates/python-base/src/app/main/aiohttp.py.hbs @@ -0,0 +1,70 @@ +"""Main application entry point.""" +# ruff: noqa: I001 + +import os + +from dotenv import load_dotenv +{{#if (eq pythonObservability "signoz")}} +from app import otel as _otel # noqa: F401 - importing activates SigNoz tracing +{{/if}} +{{#if (ne pythonLogging "none")}} +from app.logging_config import configure_logging, request_logging_middleware, add_request_id_header +{{/if}} + +{{#if (eq observability "sentry")}} +from app.observability import init_sentry + +{{/if}} +from aiohttp import web +{{#if (eq pythonObservability "prometheus-client")}} + +from app.metrics import render_metrics +{{/if}} + +load_dotenv() +{{#if (ne pythonLogging "none")}} +configure_logging() +{{/if}} + + +async def index(request: web.Request) -> web.Response: + """Root endpoint.""" + return web.json_response({"message": "Welcome to {{projectName}}!"}) + + +async def health(request: web.Request) -> web.Response: + """Health check endpoint.""" + return web.json_response({"status": "healthy"}) +{{#if (eq pythonObservability "prometheus-client")}} + + +async def metrics(request: web.Request) -> web.Response: + """Prometheus metrics endpoint.""" + payload, content_type = render_metrics() + return web.Response(body=payload, headers={"Content-Type": content_type}) +{{/if}} + + +def create_app() -> web.Application: + """Build the aiohttp application and register routes.""" + application = web.Application({{#if (ne pythonLogging "none")}}middlewares=[request_logging_middleware]{{/if}}) +{{#if (ne pythonLogging "none")}} + application.on_response_prepare.append(add_request_id_header) +{{/if}} + application.router.add_get("/", index) + application.router.add_get("/health", health) +{{#if (eq pythonObservability "prometheus-client")}} + application.router.add_get("/metrics", metrics) +{{/if}} + return application + + +app = create_app() + + +if __name__ == "__main__": + web.run_app( + app, + host=os.getenv("HOST", "0.0.0.0"), + port=int(os.getenv("PORT", "8000")), + ) diff --git a/packages/template-generator/templates/python-base/src/app/main/django.py.hbs b/packages/template-generator/templates/python-base/src/app/main/django.py.hbs new file mode 100644 index 000000000..38b92cd9a --- /dev/null +++ b/packages/template-generator/templates/python-base/src/app/main/django.py.hbs @@ -0,0 +1,185 @@ +"""Main application entry point.""" +# ruff: noqa: I001 + +import os + +from dotenv import load_dotenv +{{#if (eq pythonObservability "signoz")}} +from app import otel as _otel # noqa: F401 - importing activates SigNoz tracing +{{/if}} +{{#if (ne pythonLogging "none")}} +from app.logging_config import configure_logging +{{/if}} + +{{#if (eq observability "sentry")}} +from app.observability import init_sentry + +{{/if}} +import django +{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} +import json +{{/if}} +from django.conf import settings +from django.http import {{#if (eq pythonObservability "prometheus-client")}}HttpResponse, {{/if}}JsonResponse +from django.urls import path +{{#if (eq pythonObservability "prometheus-client")}} +from app.metrics import render_metrics +{{/if}} +{{#if (eq pythonApi "django-rest-framework")}} +from rest_framework.decorators import api_view +from rest_framework.response import Response +{{/if}} +{{#if (eq pythonApi "django-ninja")}} +from ninja import NinjaAPI +{{/if}} +{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} +from app.auth import create_access_token, verify_token +{{/if}} +{{#if (eq pythonGraphql "strawberry")}} +from strawberry.django.views import GraphQLView + +from app.graphql_schema import schema as graphql_schema +{{/if}} +{{#if (eq pythonGraphql "ariadne")}} +from ariadne_django.views import GraphQLView + +from app.ariadne_schema import schema as ariadne_schema +{{/if}} + +load_dotenv() +{{#if (ne pythonLogging "none")}} +configure_logging() +{{/if}} + +_cors_origin = os.getenv("CORS_ORIGIN") + +if not settings.configured: + settings.configure( + DEBUG=os.getenv("DEBUG", "true").lower() == "true", + ROOT_URLCONF=__name__, + SECRET_KEY=os.getenv("SECRET_KEY", "change-me-in-production"), + ALLOWED_HOSTS=["*"], + INSTALLED_APPS=[ + "django.contrib.contenttypes", + "django.contrib.auth", + "corsheaders", +{{#if (eq pythonApi "django-rest-framework")}} + "rest_framework", +{{/if}} + ], + MIDDLEWARE=[ +{{#if (ne pythonLogging "none")}} + "app.logging_config.request_logging_middleware", +{{/if}} + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + ], +{{#if (ne pythonLogging "none")}} + # Logging is configured by app.logging_config, not Django's defaults. + LOGGING_CONFIG=None, +{{/if}} + CORS_ALLOW_ALL_ORIGINS=not _cors_origin, + CORS_ALLOWED_ORIGINS=[_cors_origin] if _cors_origin else [], + ) + django.setup() + + +def root(request): + """Root endpoint.""" + return JsonResponse({"message": "Welcome to {{projectName}}!"}) + + +def health(request): + """Health check endpoint.""" + return JsonResponse({"status": "healthy"}) + +{{#if (eq pythonObservability "prometheus-client")}} + +def metrics(request): + """Prometheus metrics endpoint.""" + payload, content_type = render_metrics() + return HttpResponse(payload, content_type=content_type) + +{{/if}} +{{#if (eq pythonApi "django-rest-framework")}} + +@api_view(["GET"]) +def api_status(request): + """REST API status endpoint.""" + return Response({"status": "ok", "framework": "django-rest-framework"}) +{{/if}} +{{#if (eq pythonApi "django-ninja")}} + +ninja_api = NinjaAPI(title="{{projectName}} API") + + +@ninja_api.get("/status") +def api_status(request): + """REST API status endpoint.""" + return {"status": "ok", "framework": "django-ninja"} +{{/if}} + + +{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} +def auth_token(request): + """Create an access token.""" + if request.method != "POST": + return JsonResponse({"error": "Method not allowed"}, status=405) + body = json.loads(request.body) if request.body else {} + username = body.get("username", "") + if not username: + return JsonResponse({"error": "Username is required"}, status=400) + token = create_access_token(data={"sub": username}) + return JsonResponse({"access_token": token, "token_type": "bearer"}) + + +def auth_me(request): + """Get current authenticated user from Bearer token.""" + auth_header = request.headers.get("Authorization", "") + if not auth_header.startswith("Bearer "): + return JsonResponse({"error": "Missing or invalid authorization header"}, status=401) + token = auth_header.removeprefix("Bearer ") + payload = verify_token(token) + if payload is None: + return JsonResponse({"error": "Invalid or expired token"}, status=401) + return JsonResponse({"username": payload.get("sub"), "token_data": payload}) +{{/if}} + + +urlpatterns = [ + path("", root), + path("health", health), +{{#if (eq pythonObservability "prometheus-client")}} + path("metrics", metrics), +{{/if}} +{{#if (eq pythonApi "django-rest-framework")}} + path("api/status", api_status), +{{/if}} +{{#if (eq pythonApi "django-ninja")}} + path("api/", ninja_api.urls), +{{/if}} +{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} + path("auth/token", auth_token), + path("auth/me", auth_me), +{{/if}} +{{#if (eq pythonGraphql "strawberry")}} + path("graphql", GraphQLView.as_view(schema=graphql_schema)), +{{/if}} +{{#if (eq pythonGraphql "ariadne")}} + path("graphql", GraphQLView.as_view(schema=ariadne_schema)), +{{/if}} +] + +{{#if (eq pythonServer "gunicorn")}} +from django.core.wsgi import get_wsgi_application + +application = get_wsgi_application() +{{/if}} + + +if __name__ == "__main__": + from django.core.management import execute_from_command_line + import sys + + sys.argv = ["manage.py", "runserver", f"{os.getenv('HOST', '0.0.0.0')}:{os.getenv('PORT', '8000')}"] + execute_from_command_line(sys.argv) diff --git a/packages/template-generator/templates/python-base/src/app/main.py.hbs b/packages/template-generator/templates/python-base/src/app/main/fastapi.py.hbs similarity index 67% rename from packages/template-generator/templates/python-base/src/app/main.py.hbs rename to packages/template-generator/templates/python-base/src/app/main/fastapi.py.hbs index 7d0420846..3531ebf9f 100644 --- a/packages/template-generator/templates/python-base/src/app/main.py.hbs +++ b/packages/template-generator/templates/python-base/src/app/main/fastapi.py.hbs @@ -1,42 +1,25 @@ """Main application entry point.""" # ruff: noqa: I001 -{{#unless (or (eq pythonWebFramework "streamlit") (eq pythonWebFramework "none"))}} import os -{{/unless}} -{{#if (eq pythonWebFramework "fastapi")}} from contextlib import asynccontextmanager {{#if (or (eq pythonOrm "sqlalchemy") (eq pythonOrm "sqlmodel"))}} from typing import Annotated {{/if}} -{{/if}} -{{#if (and (eq pythonWebFramework "litestar") (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt"))))}} -from typing import Annotated - -{{/if}} from dotenv import load_dotenv {{#if (eq pythonObservability "signoz")}} from app import otel as _otel # noqa: F401 - importing activates SigNoz tracing {{/if}} {{#if (ne pythonLogging "none")}} -{{#if (eq pythonWebFramework "flask")}} -from app.logging_config import DevServerRequestHandler, RequestLoggingMiddleware, configure_logging -{{else if (or (eq pythonWebFramework "fastapi") (eq pythonWebFramework "starlette") (eq pythonWebFramework "litestar"))}} from app.logging_config import RequestLoggingMiddleware, configure_logging -{{else if (eq pythonWebFramework "aiohttp")}} -from app.logging_config import configure_logging, request_logging_middleware{{#if (eq pythonWebFramework "aiohttp")}}, add_request_id_header{{/if}} -{{else}} -from app.logging_config import configure_logging -{{/if}} {{/if}} {{#if (eq observability "sentry")}} from app.observability import init_sentry {{/if}} -{{#if (eq pythonWebFramework "fastapi")}} from fastapi import {{#if (or (eq pythonOrm "sqlalchemy") (eq pythonOrm "sqlmodel"))}}Depends, {{/if}}FastAPI{{#if (or (eq pythonOrm "sqlalchemy") (eq pythonOrm "sqlmodel") (eq pythonOrm "tortoise-orm") (and (eq pythonOrm "none") (eq pythonValidation "pydantic")) (eq pythonAuth "authlib") (eq pythonAuth "jwt") (eq pythonAuth "pyjwt"))}}, HTTPException{{/if}}{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}}, Header{{/if}} from fastapi.middleware.cors import CORSMiddleware {{#if (eq pythonObservability "signoz")}} @@ -1125,543 +1108,3 @@ if __name__ == "__main__": log_config=None, {{/if}} ) -{{/if}} -{{#if (eq pythonWebFramework "django")}} -import django -{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} -import json -{{/if}} -from django.conf import settings -from django.http import {{#if (eq pythonObservability "prometheus-client")}}HttpResponse, {{/if}}JsonResponse -from django.urls import path -{{#if (eq pythonObservability "prometheus-client")}} -from app.metrics import render_metrics -{{/if}} -{{#if (eq pythonApi "django-rest-framework")}} -from rest_framework.decorators import api_view -from rest_framework.response import Response -{{/if}} -{{#if (eq pythonApi "django-ninja")}} -from ninja import NinjaAPI -{{/if}} -{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} -from app.auth import create_access_token, verify_token -{{/if}} -{{#if (eq pythonGraphql "strawberry")}} -from strawberry.django.views import GraphQLView - -from app.graphql_schema import schema as graphql_schema -{{/if}} -{{#if (eq pythonGraphql "ariadne")}} -from ariadne_django.views import GraphQLView - -from app.ariadne_schema import schema as ariadne_schema -{{/if}} - -load_dotenv() -{{#if (ne pythonLogging "none")}} -configure_logging() -{{/if}} - -_cors_origin = os.getenv("CORS_ORIGIN") - -if not settings.configured: - settings.configure( - DEBUG=os.getenv("DEBUG", "true").lower() == "true", - ROOT_URLCONF=__name__, - SECRET_KEY=os.getenv("SECRET_KEY", "change-me-in-production"), - ALLOWED_HOSTS=["*"], - INSTALLED_APPS=[ - "django.contrib.contenttypes", - "django.contrib.auth", - "corsheaders", -{{#if (eq pythonApi "django-rest-framework")}} - "rest_framework", -{{/if}} - ], - MIDDLEWARE=[ -{{#if (ne pythonLogging "none")}} - "app.logging_config.request_logging_middleware", -{{/if}} - "corsheaders.middleware.CorsMiddleware", - "django.middleware.common.CommonMiddleware", - ], -{{#if (ne pythonLogging "none")}} - # Logging is configured by app.logging_config, not Django's defaults. - LOGGING_CONFIG=None, -{{/if}} - CORS_ALLOW_ALL_ORIGINS=not _cors_origin, - CORS_ALLOWED_ORIGINS=[_cors_origin] if _cors_origin else [], - ) - django.setup() - - -def root(request): - """Root endpoint.""" - return JsonResponse({"message": "Welcome to {{projectName}}!"}) - - -def health(request): - """Health check endpoint.""" - return JsonResponse({"status": "healthy"}) - -{{#if (eq pythonObservability "prometheus-client")}} - -def metrics(request): - """Prometheus metrics endpoint.""" - payload, content_type = render_metrics() - return HttpResponse(payload, content_type=content_type) - -{{/if}} -{{#if (eq pythonApi "django-rest-framework")}} - -@api_view(["GET"]) -def api_status(request): - """REST API status endpoint.""" - return Response({"status": "ok", "framework": "django-rest-framework"}) -{{/if}} -{{#if (eq pythonApi "django-ninja")}} - -ninja_api = NinjaAPI(title="{{projectName}} API") - - -@ninja_api.get("/status") -def api_status(request): - """REST API status endpoint.""" - return {"status": "ok", "framework": "django-ninja"} -{{/if}} - - -{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} -def auth_token(request): - """Create an access token.""" - if request.method != "POST": - return JsonResponse({"error": "Method not allowed"}, status=405) - body = json.loads(request.body) if request.body else {} - username = body.get("username", "") - if not username: - return JsonResponse({"error": "Username is required"}, status=400) - token = create_access_token(data={"sub": username}) - return JsonResponse({"access_token": token, "token_type": "bearer"}) - - -def auth_me(request): - """Get current authenticated user from Bearer token.""" - auth_header = request.headers.get("Authorization", "") - if not auth_header.startswith("Bearer "): - return JsonResponse({"error": "Missing or invalid authorization header"}, status=401) - token = auth_header.removeprefix("Bearer ") - payload = verify_token(token) - if payload is None: - return JsonResponse({"error": "Invalid or expired token"}, status=401) - return JsonResponse({"username": payload.get("sub"), "token_data": payload}) -{{/if}} - - -urlpatterns = [ - path("", root), - path("health", health), -{{#if (eq pythonObservability "prometheus-client")}} - path("metrics", metrics), -{{/if}} -{{#if (eq pythonApi "django-rest-framework")}} - path("api/status", api_status), -{{/if}} -{{#if (eq pythonApi "django-ninja")}} - path("api/", ninja_api.urls), -{{/if}} -{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} - path("auth/token", auth_token), - path("auth/me", auth_me), -{{/if}} -{{#if (eq pythonGraphql "strawberry")}} - path("graphql", GraphQLView.as_view(schema=graphql_schema)), -{{/if}} -{{#if (eq pythonGraphql "ariadne")}} - path("graphql", GraphQLView.as_view(schema=ariadne_schema)), -{{/if}} -] - -{{#if (eq pythonServer "gunicorn")}} -from django.core.wsgi import get_wsgi_application - -application = get_wsgi_application() -{{/if}} - - -if __name__ == "__main__": - from django.core.management import execute_from_command_line - import sys - - sys.argv = ["manage.py", "runserver", f"{os.getenv('HOST', '0.0.0.0')}:{os.getenv('PORT', '8000')}"] - execute_from_command_line(sys.argv) -{{/if}} -{{#if (eq pythonWebFramework "flask")}} -from flask import Flask, jsonify{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}}, request{{/if}} -from flask_cors import CORS -{{#if (eq pythonGraphql "ariadne")}} -from werkzeug.middleware.dispatcher import DispatcherMiddleware -{{/if}} -{{#if (eq pythonValidation "pydantic")}} -from app.settings import get_settings -{{/if}} -{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} -from app.auth import create_access_token, verify_token -{{/if}} -{{#if (eq pythonGraphql "strawberry")}} -from strawberry.flask.views import GraphQLView - -from app.graphql_schema import schema as graphql_schema -{{/if}} -{{#if (eq pythonGraphql "ariadne")}} -from ariadne.wsgi import GraphQL - -from app.ariadne_schema import schema as ariadne_schema -{{/if}} -{{#if (eq pythonObservability "prometheus-client")}} -from app.metrics import render_metrics -{{/if}} - -load_dotenv() -{{#if (ne pythonLogging "none")}} -configure_logging() -{{/if}} - -{{#if (eq pythonValidation "pydantic")}} -settings = get_settings() - -app = Flask(__name__) -app.config["APP_NAME"] = settings.app_name -{{else}} -app = Flask(__name__) -{{/if}} -CORS(app, origins=os.getenv("CORS_ORIGIN") or "*") -{{#if (eq pythonGraphql "strawberry")}} -app.add_url_rule("/graphql", view_func=GraphQLView.as_view("graphql_view", schema=graphql_schema)) -{{/if}} -{{#if (eq pythonGraphql "ariadne")}} -app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/graphql": GraphQL(ariadne_schema, debug=True)}) -{{/if}} -{{#if (ne pythonLogging "none")}} -app.wsgi_app = RequestLoggingMiddleware(app.wsgi_app) # type: ignore[method-assign] -{{/if}} - - -@app.route("/") -def root(): - """Root endpoint.""" - return jsonify({"message": "Welcome to {{projectName}}!"}) - - -@app.route("/health") -def health(): - """Health check endpoint.""" - return jsonify({"status": "healthy"}) - -{{#if (eq pythonObservability "prometheus-client")}} - -@app.route("/metrics") -def metrics(): - """Prometheus metrics endpoint.""" - payload, content_type = render_metrics() - return payload, 200, {"Content-Type": content_type} - -{{/if}} -{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} - -@app.route("/auth/token", methods=["POST"]) -def auth_token(): - """Create an access token.""" - data = request.get_json(silent=True) or {} - username = data.get("username", "") - if not username: - return jsonify({"error": "Username is required"}), 400 - token = create_access_token(data={"sub": username}) - return jsonify({"access_token": token, "token_type": "bearer"}) - - -@app.route("/auth/me") -def auth_me(): - """Get current authenticated user from Bearer token.""" - auth_header = request.headers.get("Authorization", "") - if not auth_header.startswith("Bearer "): - return jsonify({"error": "Missing or invalid authorization header"}), 401 - token = auth_header.removeprefix("Bearer ") - payload = verify_token(token) - if payload is None: - return jsonify({"error": "Invalid or expired token"}), 401 - return jsonify({"username": payload.get("sub"), "token_data": payload}) -{{/if}} - - -if __name__ == "__main__": - app.run( - host=os.getenv("HOST", "0.0.0.0"), - port=int(os.getenv("PORT", "8000")), - debug=os.getenv("DEBUG", "true").lower() == "true", -{{#if (ne pythonLogging "none")}} - request_handler=DevServerRequestHandler, -{{/if}} - ) -{{/if}} -{{#if (eq pythonWebFramework "litestar")}} -from litestar import Litestar, {{#if (eq pythonObservability "prometheus-client")}}Response, {{/if}}get{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}}, post{{/if}}{{#if (eq pythonGraphql "ariadne")}}, asgi{{/if}} -from litestar.config.cors import CORSConfig -{{#if (eq pythonObservability "prometheus-client")}} -from app.metrics import render_metrics -{{/if}} -{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} -from litestar.exceptions import HTTPException -from litestar.params import Parameter - -from app.auth import create_access_token, verify_token -{{/if}} -{{#if (eq pythonGraphql "strawberry")}} -from strawberry.litestar import make_graphql_controller - -from app.graphql_schema import schema as graphql_schema -{{/if}} -{{#if (eq pythonGraphql "ariadne")}} -from ariadne.asgi import GraphQL -from litestar.types import Receive, Scope, Send - -from app.ariadne_schema import schema as ariadne_schema -{{/if}} - -load_dotenv() - - -@get("/") -async def index() -> dict[str, str]: - """Root endpoint.""" - return {"message": "Hello from Litestar!"} - - -@get("/health") -async def health() -> dict[str, str]: - """Health check endpoint.""" - return {"status": "ok"} -{{#if (eq pythonObservability "prometheus-client")}} - - -@get("/metrics") -async def metrics() -> Response: - """Prometheus metrics endpoint.""" - payload, content_type = render_metrics() - return Response(content=payload, media_type=content_type) -{{/if}} - - -{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} - -@post("/auth/token") -async def auth_token(data: dict) -> dict: - """Create an access token.""" - username = data.get("username", "") - if not username: - raise HTTPException(status_code=400, detail="Username is required") - token = create_access_token(data={"sub": username}) - return {"access_token": token, "token_type": "bearer"} - - -@get("/auth/me") -async def auth_me( - authorization: Annotated[str, Parameter(header="Authorization")] = "", -) -> dict: - """Get current authenticated user from Bearer token.""" - if not authorization.startswith("Bearer "): - raise HTTPException(status_code=401, detail="Missing or invalid authorization header") - token = authorization.removeprefix("Bearer ") - payload = verify_token(token) - if payload is None: - raise HTTPException(status_code=401, detail="Invalid or expired token") - return {"username": payload.get("sub"), "token_data": payload} -{{/if}} - - -cors_config = CORSConfig(allow_origins=[os.getenv("CORS_ORIGIN") or "*"]) -{{#if (eq pythonGraphql "strawberry")}} -GraphQLController = make_graphql_controller(graphql_schema, path="/graphql") -{{/if}} -{{#if (eq pythonGraphql "ariadne")}} -ariadne_app = GraphQL(ariadne_schema, debug=True) - - -@asgi("/graphql", is_mount=True, copy_scope=True) -async def graphql(scope: Scope, receive: Receive, send: Send) -> None: - """Mount Ariadne's ASGI GraphQL app.""" - await ariadne_app(scope, receive, send) -{{/if}} -app = Litestar( - route_handlers=[index, health{{#if (eq pythonObservability "prometheus-client")}}, metrics{{/if}}{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}}, auth_token, auth_me{{/if}}{{#if (eq pythonGraphql "strawberry")}}, GraphQLController{{/if}}{{#if (eq pythonGraphql "ariadne")}}, graphql{{/if}}], - cors_config=cors_config, -{{#if (ne pythonLogging "none")}} - # `litestar run` lets Uvicorn configure its loggers after this module loads. - on_startup=[configure_logging], -{{/if}} -) -{{#if (ne pythonLogging "none")}} -# Wrap the whole app rather than its routes, so CORS preflights and 404s are logged too. -app.asgi_handler = RequestLoggingMiddleware(app.asgi_handler) -# Litestar configures logging while it builds the app; route its loggers afterwards. -configure_logging() -{{/if}} -{{/if}} -{{#if (eq pythonWebFramework "starlette")}} -from starlette.applications import Starlette -from starlette.middleware import Middleware -from starlette.middleware.cors import CORSMiddleware -from starlette.requests import Request -from starlette.responses import JSONResponse{{#if (eq pythonObservability "prometheus-client")}}, Response{{/if}} -from starlette.routing import Route -{{#if (eq pythonObservability "prometheus-client")}} -from app.metrics import render_metrics -{{/if}} - -load_dotenv() -{{#if (ne pythonLogging "none")}} -configure_logging() -{{/if}} - - -async def index(request: Request) -> JSONResponse: - """Root endpoint.""" - return JSONResponse({"message": "Hello from Starlette!"}) - - -async def health(request: Request) -> JSONResponse: - """Health check endpoint.""" - return JSONResponse({"status": "ok"}) -{{#if (eq pythonObservability "prometheus-client")}} - - -async def metrics(request: Request) -> Response: - """Prometheus metrics endpoint.""" - payload, content_type = render_metrics() - return Response(payload, media_type=content_type) -{{/if}} - - -middleware = [ -{{#if (ne pythonLogging "none")}} - # First in the list runs outermost, so CORS preflight replies are logged too. - Middleware(RequestLoggingMiddleware), -{{/if}} - Middleware( - CORSMiddleware, - allow_origins=[os.getenv("CORS_ORIGIN") or "*"], - allow_methods=["*"], - allow_headers=["*"], - ), -] - -app = Starlette( - routes=[ - Route("/", index), - Route("/health", health), -{{#if (eq pythonObservability "prometheus-client")}} - Route("/metrics", metrics), -{{/if}} - ], - middleware=middleware, -) - - -if __name__ == "__main__": - import uvicorn - -{{#if (ne pythonLogging "none")}} - uvicorn.run( - app, - host=os.getenv("HOST", "0.0.0.0"), - port=int(os.getenv("PORT", "8000")), - log_config=None, - ) -{{else}} - uvicorn.run(app, host=os.getenv("HOST", "0.0.0.0"), port=int(os.getenv("PORT", "8000"))) -{{/if}} -{{/if}} -{{#if (eq pythonWebFramework "aiohttp")}} -from aiohttp import web -{{#if (eq pythonObservability "prometheus-client")}} - -from app.metrics import render_metrics -{{/if}} - -load_dotenv() -{{#if (ne pythonLogging "none")}} -configure_logging() -{{/if}} - - -async def index(request: web.Request) -> web.Response: - """Root endpoint.""" - return web.json_response({"message": "Welcome to {{projectName}}!"}) - - -async def health(request: web.Request) -> web.Response: - """Health check endpoint.""" - return web.json_response({"status": "healthy"}) -{{#if (eq pythonObservability "prometheus-client")}} - - -async def metrics(request: web.Request) -> web.Response: - """Prometheus metrics endpoint.""" - payload, content_type = render_metrics() - return web.Response(body=payload, headers={"Content-Type": content_type}) -{{/if}} - - -def create_app() -> web.Application: - """Build the aiohttp application and register routes.""" - application = web.Application({{#if (ne pythonLogging "none")}}middlewares=[request_logging_middleware]{{/if}}) -{{#if (ne pythonLogging "none")}} - application.on_response_prepare.append(add_request_id_header) -{{/if}} - application.router.add_get("/", index) - application.router.add_get("/health", health) -{{#if (eq pythonObservability "prometheus-client")}} - application.router.add_get("/metrics", metrics) -{{/if}} - return application - - -app = create_app() - - -if __name__ == "__main__": - web.run_app( - app, - host=os.getenv("HOST", "0.0.0.0"), - port=int(os.getenv("PORT", "8000")), - ) -{{/if}} -{{#if (eq pythonWebFramework "streamlit")}} -import streamlit as st - -load_dotenv() - -st.set_page_config(page_title="{{projectName}}", page_icon="🐍", layout="wide") -st.title("{{projectName}}") -st.caption("Generated with Better Fullstack") -st.success("Your Streamlit application is ready.") - -with st.expander("Next step"): - st.write("Replace this starter with your data, charts, forms, or AI workflow.") -{{/if}} -{{#if (eq pythonWebFramework "none")}} -load_dotenv() -{{#if (ne pythonLogging "none")}} -configure_logging() -{{/if}} - - -def main(): - """Main entry point.""" - print("Welcome to {{projectName}}!") - print("Add your application logic here.") - - -if __name__ == "__main__": - main() -{{/if}} diff --git a/packages/template-generator/templates/python-base/src/app/main/flask.py.hbs b/packages/template-generator/templates/python-base/src/app/main/flask.py.hbs new file mode 100644 index 000000000..68c7d75f7 --- /dev/null +++ b/packages/template-generator/templates/python-base/src/app/main/flask.py.hbs @@ -0,0 +1,123 @@ +"""Main application entry point.""" +# ruff: noqa: I001 + +import os + +from dotenv import load_dotenv +{{#if (eq pythonObservability "signoz")}} +from app import otel as _otel # noqa: F401 - importing activates SigNoz tracing +{{/if}} +{{#if (ne pythonLogging "none")}} +from app.logging_config import DevServerRequestHandler, RequestLoggingMiddleware, configure_logging +{{/if}} + +{{#if (eq observability "sentry")}} +from app.observability import init_sentry + +{{/if}} +from flask import Flask, jsonify{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}}, request{{/if}} +from flask_cors import CORS +{{#if (eq pythonGraphql "ariadne")}} +from werkzeug.middleware.dispatcher import DispatcherMiddleware +{{/if}} +{{#if (eq pythonValidation "pydantic")}} +from app.settings import get_settings +{{/if}} +{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} +from app.auth import create_access_token, verify_token +{{/if}} +{{#if (eq pythonGraphql "strawberry")}} +from strawberry.flask.views import GraphQLView + +from app.graphql_schema import schema as graphql_schema +{{/if}} +{{#if (eq pythonGraphql "ariadne")}} +from ariadne.wsgi import GraphQL + +from app.ariadne_schema import schema as ariadne_schema +{{/if}} +{{#if (eq pythonObservability "prometheus-client")}} +from app.metrics import render_metrics +{{/if}} + +load_dotenv() +{{#if (ne pythonLogging "none")}} +configure_logging() +{{/if}} + +{{#if (eq pythonValidation "pydantic")}} +settings = get_settings() + +app = Flask(__name__) +app.config["APP_NAME"] = settings.app_name +{{else}} +app = Flask(__name__) +{{/if}} +CORS(app, origins=os.getenv("CORS_ORIGIN") or "*") +{{#if (eq pythonGraphql "strawberry")}} +app.add_url_rule("/graphql", view_func=GraphQLView.as_view("graphql_view", schema=graphql_schema)) +{{/if}} +{{#if (eq pythonGraphql "ariadne")}} +app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/graphql": GraphQL(ariadne_schema, debug=True)}) +{{/if}} +{{#if (ne pythonLogging "none")}} +app.wsgi_app = RequestLoggingMiddleware(app.wsgi_app) # type: ignore[method-assign] +{{/if}} + + +@app.route("/") +def root(): + """Root endpoint.""" + return jsonify({"message": "Welcome to {{projectName}}!"}) + + +@app.route("/health") +def health(): + """Health check endpoint.""" + return jsonify({"status": "healthy"}) + +{{#if (eq pythonObservability "prometheus-client")}} + +@app.route("/metrics") +def metrics(): + """Prometheus metrics endpoint.""" + payload, content_type = render_metrics() + return payload, 200, {"Content-Type": content_type} + +{{/if}} +{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} + +@app.route("/auth/token", methods=["POST"]) +def auth_token(): + """Create an access token.""" + data = request.get_json(silent=True) or {} + username = data.get("username", "") + if not username: + return jsonify({"error": "Username is required"}), 400 + token = create_access_token(data={"sub": username}) + return jsonify({"access_token": token, "token_type": "bearer"}) + + +@app.route("/auth/me") +def auth_me(): + """Get current authenticated user from Bearer token.""" + auth_header = request.headers.get("Authorization", "") + if not auth_header.startswith("Bearer "): + return jsonify({"error": "Missing or invalid authorization header"}), 401 + token = auth_header.removeprefix("Bearer ") + payload = verify_token(token) + if payload is None: + return jsonify({"error": "Invalid or expired token"}), 401 + return jsonify({"username": payload.get("sub"), "token_data": payload}) +{{/if}} + + +if __name__ == "__main__": + app.run( + host=os.getenv("HOST", "0.0.0.0"), + port=int(os.getenv("PORT", "8000")), + debug=os.getenv("DEBUG", "true").lower() == "true", +{{#if (ne pythonLogging "none")}} + request_handler=DevServerRequestHandler, +{{/if}} + ) diff --git a/packages/template-generator/templates/python-base/src/app/main/litestar.py.hbs b/packages/template-generator/templates/python-base/src/app/main/litestar.py.hbs new file mode 100644 index 000000000..3dc7b935a --- /dev/null +++ b/packages/template-generator/templates/python-base/src/app/main/litestar.py.hbs @@ -0,0 +1,122 @@ +"""Main application entry point.""" +# ruff: noqa: I001 + +import os + +{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} +from typing import Annotated + +{{/if}} +from dotenv import load_dotenv +{{#if (eq pythonObservability "signoz")}} +from app import otel as _otel # noqa: F401 - importing activates SigNoz tracing +{{/if}} +{{#if (ne pythonLogging "none")}} +from app.logging_config import RequestLoggingMiddleware, configure_logging +{{/if}} + +{{#if (eq observability "sentry")}} +from app.observability import init_sentry + +{{/if}} +from litestar import Litestar, {{#if (eq pythonObservability "prometheus-client")}}Response, {{/if}}get{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}}, post{{/if}}{{#if (eq pythonGraphql "ariadne")}}, asgi{{/if}} +from litestar.config.cors import CORSConfig +{{#if (eq pythonObservability "prometheus-client")}} +from app.metrics import render_metrics +{{/if}} +{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} +from litestar.exceptions import HTTPException +from litestar.params import Parameter + +from app.auth import create_access_token, verify_token +{{/if}} +{{#if (eq pythonGraphql "strawberry")}} +from strawberry.litestar import make_graphql_controller + +from app.graphql_schema import schema as graphql_schema +{{/if}} +{{#if (eq pythonGraphql "ariadne")}} +from ariadne.asgi import GraphQL +from litestar.types import Receive, Scope, Send + +from app.ariadne_schema import schema as ariadne_schema +{{/if}} + +load_dotenv() + + +@get("/") +async def index() -> dict[str, str]: + """Root endpoint.""" + return {"message": "Hello from Litestar!"} + + +@get("/health") +async def health() -> dict[str, str]: + """Health check endpoint.""" + return {"status": "ok"} +{{#if (eq pythonObservability "prometheus-client")}} + + +@get("/metrics") +async def metrics() -> Response: + """Prometheus metrics endpoint.""" + payload, content_type = render_metrics() + return Response(content=payload, media_type=content_type) +{{/if}} + + +{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}} + +@post("/auth/token") +async def auth_token(data: dict) -> dict: + """Create an access token.""" + username = data.get("username", "") + if not username: + raise HTTPException(status_code=400, detail="Username is required") + token = create_access_token(data={"sub": username}) + return {"access_token": token, "token_type": "bearer"} + + +@get("/auth/me") +async def auth_me( + authorization: Annotated[str, Parameter(header="Authorization")] = "", +) -> dict: + """Get current authenticated user from Bearer token.""" + if not authorization.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Missing or invalid authorization header") + token = authorization.removeprefix("Bearer ") + payload = verify_token(token) + if payload is None: + raise HTTPException(status_code=401, detail="Invalid or expired token") + return {"username": payload.get("sub"), "token_data": payload} +{{/if}} + + +cors_config = CORSConfig(allow_origins=[os.getenv("CORS_ORIGIN") or "*"]) +{{#if (eq pythonGraphql "strawberry")}} +GraphQLController = make_graphql_controller(graphql_schema, path="/graphql") +{{/if}} +{{#if (eq pythonGraphql "ariadne")}} +ariadne_app = GraphQL(ariadne_schema, debug=True) + + +@asgi("/graphql", is_mount=True, copy_scope=True) +async def graphql(scope: Scope, receive: Receive, send: Send) -> None: + """Mount Ariadne's ASGI GraphQL app.""" + await ariadne_app(scope, receive, send) +{{/if}} +app = Litestar( + route_handlers=[index, health{{#if (eq pythonObservability "prometheus-client")}}, metrics{{/if}}{{#if (or (eq pythonAuth "authlib") (or (eq pythonAuth "jwt") (eq pythonAuth "pyjwt")))}}, auth_token, auth_me{{/if}}{{#if (eq pythonGraphql "strawberry")}}, GraphQLController{{/if}}{{#if (eq pythonGraphql "ariadne")}}, graphql{{/if}}], + cors_config=cors_config, +{{#if (ne pythonLogging "none")}} + # `litestar run` lets Uvicorn configure its loggers after this module loads. + on_startup=[configure_logging], +{{/if}} +) +{{#if (ne pythonLogging "none")}} +# Wrap the whole app rather than its routes, so CORS preflights and 404s are logged too. +app.asgi_handler = RequestLoggingMiddleware(app.asgi_handler) +# Litestar configures logging while it builds the app; route its loggers afterwards. +configure_logging() +{{/if}} diff --git a/packages/template-generator/templates/python-base/src/app/main/none.py.hbs b/packages/template-generator/templates/python-base/src/app/main/none.py.hbs new file mode 100644 index 000000000..0b700a459 --- /dev/null +++ b/packages/template-generator/templates/python-base/src/app/main/none.py.hbs @@ -0,0 +1,30 @@ +"""Main application entry point.""" +# ruff: noqa: I001 + + +from dotenv import load_dotenv +{{#if (eq pythonObservability "signoz")}} +from app import otel as _otel # noqa: F401 - importing activates SigNoz tracing +{{/if}} +{{#if (ne pythonLogging "none")}} +from app.logging_config import configure_logging +{{/if}} + +{{#if (eq observability "sentry")}} +from app.observability import init_sentry + +{{/if}} +load_dotenv() +{{#if (ne pythonLogging "none")}} +configure_logging() +{{/if}} + + +def main(): + """Main entry point.""" + print("Welcome to {{projectName}}!") + print("Add your application logic here.") + + +if __name__ == "__main__": + main() diff --git a/packages/template-generator/templates/python-base/src/app/main/starlette.py.hbs b/packages/template-generator/templates/python-base/src/app/main/starlette.py.hbs new file mode 100644 index 000000000..891a43af7 --- /dev/null +++ b/packages/template-generator/templates/python-base/src/app/main/starlette.py.hbs @@ -0,0 +1,89 @@ +"""Main application entry point.""" +# ruff: noqa: I001 + +import os + +from dotenv import load_dotenv +{{#if (eq pythonObservability "signoz")}} +from app import otel as _otel # noqa: F401 - importing activates SigNoz tracing +{{/if}} +{{#if (ne pythonLogging "none")}} +from app.logging_config import RequestLoggingMiddleware, configure_logging +{{/if}} + +{{#if (eq observability "sentry")}} +from app.observability import init_sentry + +{{/if}} +from starlette.applications import Starlette +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse{{#if (eq pythonObservability "prometheus-client")}}, Response{{/if}} +from starlette.routing import Route +{{#if (eq pythonObservability "prometheus-client")}} +from app.metrics import render_metrics +{{/if}} + +load_dotenv() +{{#if (ne pythonLogging "none")}} +configure_logging() +{{/if}} + + +async def index(request: Request) -> JSONResponse: + """Root endpoint.""" + return JSONResponse({"message": "Hello from Starlette!"}) + + +async def health(request: Request) -> JSONResponse: + """Health check endpoint.""" + return JSONResponse({"status": "ok"}) +{{#if (eq pythonObservability "prometheus-client")}} + + +async def metrics(request: Request) -> Response: + """Prometheus metrics endpoint.""" + payload, content_type = render_metrics() + return Response(payload, media_type=content_type) +{{/if}} + + +middleware = [ +{{#if (ne pythonLogging "none")}} + # First in the list runs outermost, so CORS preflight replies are logged too. + Middleware(RequestLoggingMiddleware), +{{/if}} + Middleware( + CORSMiddleware, + allow_origins=[os.getenv("CORS_ORIGIN") or "*"], + allow_methods=["*"], + allow_headers=["*"], + ), +] + +app = Starlette( + routes=[ + Route("/", index), + Route("/health", health), +{{#if (eq pythonObservability "prometheus-client")}} + Route("/metrics", metrics), +{{/if}} + ], + middleware=middleware, +) + + +if __name__ == "__main__": + import uvicorn + +{{#if (ne pythonLogging "none")}} + uvicorn.run( + app, + host=os.getenv("HOST", "0.0.0.0"), + port=int(os.getenv("PORT", "8000")), + log_config=None, + ) +{{else}} + uvicorn.run(app, host=os.getenv("HOST", "0.0.0.0"), port=int(os.getenv("PORT", "8000"))) +{{/if}} diff --git a/packages/template-generator/templates/python-base/src/app/main/streamlit.py.hbs b/packages/template-generator/templates/python-base/src/app/main/streamlit.py.hbs new file mode 100644 index 000000000..5cfee1079 --- /dev/null +++ b/packages/template-generator/templates/python-base/src/app/main/streamlit.py.hbs @@ -0,0 +1,27 @@ +"""Main application entry point.""" +# ruff: noqa: I001 + + +from dotenv import load_dotenv +{{#if (eq pythonObservability "signoz")}} +from app import otel as _otel # noqa: F401 - importing activates SigNoz tracing +{{/if}} +{{#if (ne pythonLogging "none")}} +from app.logging_config import configure_logging +{{/if}} + +{{#if (eq observability "sentry")}} +from app.observability import init_sentry + +{{/if}} +import streamlit as st + +load_dotenv() + +st.set_page_config(page_title="{{projectName}}", page_icon="🐍", layout="wide") +st.title("{{projectName}}") +st.caption("Generated with Better Fullstack") +st.success("Your Streamlit application is ready.") + +with st.expander("Next step"): + st.write("Replace this starter with your data, charts, forms, or AI workflow.")