diff --git a/.gitignore b/.gitignore index cf997a8..c4ee0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -135,7 +135,7 @@ celerybeat.pid *.sage.py # Environments -.env +.env** .envrc .venv* env/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 9e26dfe..61f1747 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,9 @@ -{} \ No newline at end of file +{ + "python-envs.pythonProjects": [ + { + "path": "", + "envManager": "ms-python.python:poetry", + "packageManager": "ms-python.python:poetry" + } + ] +} diff --git a/pyproject.toml b/pyproject.toml index aa2658c..294009c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,19 +28,14 @@ mongo = ["pymongo"] [tool.poetry] packages = [ { include = "sample", from = "src" }, - { include = "features", from = "src" }, - { include = "utils", from = "src" }, - { include = "api", from = "src" }, - { include = "db", from = "src" }, - { include = "templates", from = "." }, { include = "assets", from = "." }, ] - +include = [{ path = "templates/**/*", format = ["sdist", "wheel"] }] exclude = ["src/**/__pycache__", "tests", "docs", "*.log"] [project.scripts] sample = "sample.cli:cli" -lint = "utils.lint:main" +lint = "sample.utils.lint:main" [tool.poetry.group.dev.dependencies] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..253daf3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,20 @@ +annotated-doc==0.0.4 ; python_version >= "3.10" and python_version < "3.13" +annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "3.13" +anyio==4.12.1 ; python_version >= "3.10" and python_version < "3.13" +click==8.3.1 ; python_version >= "3.10" and python_version < "3.13" +colorama==0.4.6 ; python_version >= "3.10" and python_version < "3.13" and platform_system == "Windows" +dotenv==0.9.9 ; python_version >= "3.10" and python_version < "3.13" +exceptiongroup==1.3.1 ; python_version == "3.10" +fastapi==0.121.3 ; python_version >= "3.10" and python_version < "3.13" +h11==0.16.0 ; python_version >= "3.10" and python_version < "3.13" +idna==3.11 ; python_version >= "3.10" and python_version < "3.13" +jinja2==3.1.6 ; python_version >= "3.10" and python_version < "3.13" +markupsafe==3.0.3 ; python_version >= "3.10" and python_version < "3.13" +pydantic-core==2.41.5 ; python_version >= "3.10" and python_version < "3.13" +pydantic==2.12.5 ; python_version >= "3.10" and python_version < "3.13" +python-box==7.3.2 ; python_version >= "3.10" and python_version < "3.13" +python-dotenv==1.2.1 ; python_version >= "3.10" and python_version < "3.13" +starlette==0.50.0 ; python_version >= "3.10" and python_version < "3.13" +typing-extensions==4.15.0 ; python_version >= "3.10" and python_version < "3.13" +typing-inspection==0.4.2 ; python_version >= "3.10" and python_version < "3.13" +uvicorn==0.40.0 ; python_version >= "3.10" and python_version < "3.13" diff --git a/src/__init__.py b/src/__init__.py index 0ed1344..bdf1df2 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,3 +1,3 @@ -from utils._version import get_version +from sample.utils._version import get_version __version__ = get_version() diff --git a/src/api/__init__.py b/src/api/__init__.py deleted file mode 100644 index 0ed1344..0000000 --- a/src/api/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from utils._version import get_version - -__version__ = get_version() diff --git a/src/sample/__init__.py b/src/sample/__init__.py index 143c9a4..20ec7fe 100644 --- a/src/sample/__init__.py +++ b/src/sample/__init__.py @@ -1,3 +1,3 @@ -from utils._version import get_version +from sample.utils._version import get_version __version__ = get_version() diff --git a/src/sample/__main__.py b/src/sample/__main__.py index 942bc01..ab1001d 100644 --- a/src/sample/__main__.py +++ b/src/sample/__main__.py @@ -1,72 +1,12 @@ -import logging -from contextlib import asynccontextmanager -from pathlib import Path - -from db.connection import connect_db -from fastapi import FastAPI, Request -from fastapi.responses import HTMLResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from utils.constants import PORT - -from . import __version__ - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Startup logic - print("๐Ÿท๏ธ Sample version:", __version__) - logging.info("Starting FastAPI server...") - try: - db = connect_db() - print(f"Using database: {db.name}") - except Exception as e: - logging.error(f"โš ๏ธ Database connection failed: {e}") - print("App is ready.") - - yield # <-- control passes to request handling - - # Shutdown logic - logging.info("Shutting down FastAPI server...") - # If you want to close DB connections, do it here - - -# Create FastAPI app with lifespan handler -app = FastAPI(lifespan=lifespan) - -# Mount assets and pages -app.mount("/static", StaticFiles(directory="assets"), name="static") - -# Point Jinja2 to your templates directory -templates = Jinja2Templates( - directory=str(Path(__file__).resolve().parents[2] / "templates") -) - - -@app.get("/", response_class=HTMLResponse) -async def index(request: Request): - """Serve the main index.html at root.""" - return templates.TemplateResponse("index.html", {"request": request}) - - -@app.get("/faq", response_class=HTMLResponse) -async def faq(request: Request): - return templates.TemplateResponse("faq.html", {"request": request}) - - -templates = Jinja2Templates(directory="templates") - - -@app.get("/{full_path:path}", response_class=HTMLResponse) -async def catch_all(request: Request, full_path: str): - return templates.TemplateResponse("404.html", {"request": request}, status_code=404) +from sample.utils.constants import PORT def main(): """Entry point for CLI dev command.""" - import uvicorn + from sample.api.main import start - uvicorn.run("sample.__main__:app", host="127.0.0.1", port=PORT, reload=True) + print(f"๐Ÿš€ Starting Sample app on port {PORT}...\n") + start() if __name__ == "__main__": diff --git a/src/sample/api/__init__.py b/src/sample/api/__init__.py new file mode 100644 index 0000000..bdf1df2 --- /dev/null +++ b/src/sample/api/__init__.py @@ -0,0 +1,3 @@ +from sample.utils._version import get_version + +__version__ = get_version() diff --git a/src/sample/api/main.py b/src/sample/api/main.py new file mode 100644 index 0000000..e1bcef2 --- /dev/null +++ b/src/sample/api/main.py @@ -0,0 +1,50 @@ +import logging +from contextlib import asynccontextmanager +from pathlib import Path + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from sample.api.routes import greet_router +from sample.api.web_page import web_router +from sample.db.connection import connect_db +from sample.utils.constants import PORT + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logging.info("Starting FastAPI server...") + try: + db = connect_db() + print(f"Using database: {db.name}") + except Exception as e: + logging.error(f"โš ๏ธ Database connection failed: {e}") + + yield + + logging.info("Shutting down FastAPI server...") + + +app = FastAPI( + title="Sample API", + lifespan=lifespan, +) + +# Static +app.mount("/static", StaticFiles(directory="assets"), name="static") + +# Templates +templates = Jinja2Templates( + directory=str(Path(__file__).resolve().parent / "templates") +) + + +app.include_router(web_router) +app.include_router(greet_router) + + +def start(): + import uvicorn + + uvicorn.run("sample.api.main:app", host="127.0.0.1", port=PORT, reload=True) diff --git a/src/api/fast_api.py b/src/sample/api/routes.py similarity index 50% rename from src/api/fast_api.py rename to src/sample/api/routes.py index cd464d4..2d43ff0 100644 --- a/src/api/fast_api.py +++ b/src/sample/api/routes.py @@ -1,30 +1,17 @@ from fastapi import APIRouter, FastAPI, HTTPException, Request -from fastapi.responses import HTMLResponse, JSONResponse from pydantic import BaseModel -from starlette.exceptions import HTTPException as StarletteHTTPException -from utils.constants import API_PREFIX, GREETING -from utils.helper import normalize_name - -from . import __version__ +from sample.utils.constants import API_PREFIX, GREETING, PORT +from sample.utils.helper import normalize_name +from starlette.responses import HTMLResponse, JSONResponse +from sample import __version__ app = FastAPI( - title="sample API", - description="API endpoints for Sample with rate limiting", + title="Sample API", version=__version__, - redoc_url="/redoc", - swagger_ui_parameters={ - "docExpansion": "none", # Collapse all endpoints by default - "defaultModelsExpandDepth": -1, # Hide schemas section - "displayRequestDuration": True, # Show request duration - "layout": "BaseLayout", # Clean layout - "syntaxHighlight": {"theme": "obsidian"}, # Dark theme - }, ) -api_v1 = APIRouter( - prefix=API_PREFIX, - tags=["V1"], -) +api_router = APIRouter() +greet_router = APIRouter(prefix=API_PREFIX, tags=["V1"]) class GreetRequest(BaseModel): @@ -35,12 +22,27 @@ class GreetResponse(BaseModel): message: str -@app.get("/version", tags=["Version"]) +@api_router.get("/health") +def health_check(): + return {"status": "ok"} + + +@greet_router.post("/greet", response_model=GreetResponse) +def greet_user(payload: GreetRequest): + clean_name = normalize_name(payload.name) + + if not clean_name: + raise HTTPException(status_code=400, detail="Invalid name provided") + + return {"message": f"{GREETING}, {clean_name} ๐Ÿ‘‹"} + + +@api_router.get("/version", tags=["Version"]) def version(): - return {"version": app.version} + return {"version": api_router.version} -@app.get("/", response_class=HTMLResponse, tags=["Home"]) +@api_router.get("/", response_class=HTMLResponse, tags=["Home"]) async def read_root(request: Request): return """ @@ -82,12 +84,7 @@ async def read_root(request: Request): """ -@app.get("/health", tags=["Help"]) -def health_check(): - return {"status": "ok"} - - -@api_v1.get("/help", tags=["Help"]) +@api_router.get("/help", tags=["Help"]) def get_help(): return JSONResponse( status_code=200, @@ -97,51 +94,11 @@ def get_help(): ) -@api_v1.post("/greet", response_model=GreetResponse) -def greet_user(payload: GreetRequest): - clean_name = normalize_name(payload.name) - - if not clean_name: - raise HTTPException(status_code=400, detail="Invalid name provided") - - return {"message": f"{GREETING}, {clean_name} ๐Ÿ‘‹"} - - -@app.exception_handler(StarletteHTTPException) -async def http_exception_handler(request: Request, exc: StarletteHTTPException): - if exc.status_code == 404: - endpoint = request.url.path - - return JSONResponse( - status_code=404, - content={ - "error": f"Endpoint '{endpoint}' does not exist, use /docs to see available endpoints.", - "status": 404, - }, - ) - - # fallback for other HTTP errors - return JSONResponse( - status_code=exc.status_code, - content={"error": exc.detail}, - ) - - -# suppress chrome log -@app.get("/.well-known/appspecific/com.chrome.devtools.json") -async def chrome_devtools_probe(): - return JSONResponse({}) - - -app.include_router(api_v1) +app.include_router(api_router) +app.include_router(greet_router) def start(): import uvicorn - print(f"๐Ÿงต {__version__}\n") - uvicorn.run("api.fast_api:app", host="127.0.0.1", port=5000, reload=True) - - -if __name__ == "__main__": - start() + uvicorn.run("sample.api.routes:app", host="127.0.0.1", port=PORT, reload=True) diff --git a/src/sample/api/web_page.py b/src/sample/api/web_page.py new file mode 100644 index 0000000..824f016 --- /dev/null +++ b/src/sample/api/web_page.py @@ -0,0 +1,26 @@ +from pathlib import Path + +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates + +web_router = APIRouter() + +templates = Jinja2Templates( + directory=str(Path(__file__).resolve().parents[3] / "templates") +) + + +@web_router.get("/", response_class=HTMLResponse) +async def index(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) + + +@web_router.get("/faq", response_class=HTMLResponse) +async def faq(request: Request): + return templates.TemplateResponse("faq.html", {"request": request}) + + +@web_router.get("/{full_path:path}", response_class=HTMLResponse) +async def catch_all(request: Request, full_path: str): + return templates.TemplateResponse("404.html", {"request": request}, status_code=404) diff --git a/src/sample/cli.py b/src/sample/cli.py index cfdffda..c0a0746 100644 --- a/src/sample/cli.py +++ b/src/sample/cli.py @@ -24,6 +24,6 @@ def dev(): @cli.command() def api(): """Run the Sample FastAPI backend.""" - from api.fast_api import start + from sample.api.routes import start start() diff --git a/src/db/__init__.py b/src/sample/db/__init__.py similarity index 100% rename from src/db/__init__.py rename to src/sample/db/__init__.py diff --git a/src/db/connection.py b/src/sample/db/connection.py similarity index 94% rename from src/db/connection.py rename to src/sample/db/connection.py index c80a860..a96e670 100644 --- a/src/db/connection.py +++ b/src/sample/db/connection.py @@ -1,7 +1,7 @@ import io from datetime import datetime -from utils.constants import MONGO_CONFIG +from sample.utils.constants import MONGO_CONFIG _mongo_client = None _db = None @@ -28,7 +28,6 @@ def connect_db(): try: uri = MONGO_CONFIG["MONGODB_URI"] name = MONGO_CONFIG["DATABASE_NAME"] - print("Mongo URI =", repr(uri)) _mongo_client = MongoClient(uri, serverSelectionTimeoutMS=3000) # Force an actual connection attempt diff --git a/src/features/__init__.py b/src/sample/utils/__init__.py similarity index 100% rename from src/features/__init__.py rename to src/sample/utils/__init__.py diff --git a/src/utils/_version.py b/src/sample/utils/_version.py similarity index 100% rename from src/utils/_version.py rename to src/sample/utils/_version.py diff --git a/src/sample/utils/config.py b/src/sample/utils/config.py new file mode 100644 index 0000000..9735d77 --- /dev/null +++ b/src/sample/utils/config.py @@ -0,0 +1,15 @@ +import os + +from dotenv import load_dotenv + + +def load_env(): + env = os.getenv("ENV", "development") + + file_map = { + "production": ".env.production", + "development": ".env.development", + } + + load_dotenv(file_map.get(env, ".env.development"), override=True) + return env diff --git a/src/utils/constants.py b/src/sample/utils/constants.py similarity index 62% rename from src/utils/constants.py rename to src/sample/utils/constants.py index ace9477..15b54c1 100644 --- a/src/utils/constants.py +++ b/src/sample/utils/constants.py @@ -2,9 +2,9 @@ import os from pathlib import Path -from dotenv import load_dotenv +from sample.utils.config import load_env -load_dotenv() +load_env() # These should be in environment variables or .env file. DEFAULT_PORT = 8005 DEFAULT_API_PREFIX = "/api/v1" @@ -30,15 +30,19 @@ def safe_get(env_key: str, default) -> str: return value -MONGODB_URI = safe_get("MONGODB_URI", DEFAULT_MONGODB_URI) -DATABASE_NAME = safe_get("DATABASE_NAME", DEFAULT_DATABASE_NAME) API_PREFIX = safe_get("API_PREFIX", DEFAULT_API_PREFIX) PORT = int(safe_get("PORT", DEFAULT_PORT)) -print("MongoDB URI:", MONGODB_URI) -print("Database Name:", DATABASE_NAME) -MONGO_CONFIG = { - "MONGODB_URI": MONGODB_URI, - "DATABASE_NAME": DATABASE_NAME, -} +def get_mongo_config(): + return { + "MONGODB_URI": safe_get("MONGODB_URI", DEFAULT_MONGODB_URI), + "DATABASE_NAME": safe_get("DATABASE_NAME", DEFAULT_DATABASE_NAME), + } + + +MONGO_CONFIG = get_mongo_config() +# === Environment Selection === +ENVIRONMENT = safe_get("ENVIRONMENT", "development").lower() +logging.info(f"Environment: {ENVIRONMENT}", extra={"color": "yellow"}) +logging.info(f"Project root: {PROJECT_ROOT}", extra={"color": "yellow"}) diff --git a/src/utils/debug.py b/src/sample/utils/debug.py similarity index 100% rename from src/utils/debug.py rename to src/sample/utils/debug.py diff --git a/src/utils/helper.py b/src/sample/utils/helper.py similarity index 100% rename from src/utils/helper.py rename to src/sample/utils/helper.py diff --git a/src/utils/lint.py b/src/sample/utils/lint.py similarity index 100% rename from src/utils/lint.py rename to src/sample/utils/lint.py diff --git a/src/utils/load_messages.py b/src/sample/utils/load_messages.py similarity index 95% rename from src/utils/load_messages.py rename to src/sample/utils/load_messages.py index a35c171..08c3b5a 100644 --- a/src/utils/load_messages.py +++ b/src/sample/utils/load_messages.py @@ -1,9 +1,9 @@ # utils/get_message.py -from box import Box import importlib +from box import Box -# auto-collect static and text blocks from utils.static +# auto-collect static and text blocks from sample.utils.static _static_module = importlib.import_module("utils.templates") _TEMPLATE_MAP = {} for name, val in vars(_static_module).items(): diff --git a/src/utils/templates.py b/src/sample/utils/templates.py similarity index 100% rename from src/utils/templates.py rename to src/sample/utils/templates.py diff --git a/src/utils/__init__.py b/src/utils/__init__.py deleted file mode 100644 index e69de29..0000000