54132cc
commit 54132cc
23 files changed
+1529 -0
Showing 23 changed files+1529-0
docs_site/src/content/docs/advanced/additional-responses.mddocs_site/src/content/docs/advanced/advanced-dependencies.mddocs_site/src/content/docs/advanced/advanced-middleware.mddocs_site/src/content/docs/advanced/advanced-security.mddocs_site/src/content/docs/advanced/async-tests.mddocs_site/src/content/docs/advanced/behind-a-proxy.mddocs_site/src/content/docs/advanced/custom-response.mddocs_site/src/content/docs/advanced/dataclasses.mddocs_site/src/content/docs/advanced/generate-clients.mddocs_site/src/content/docs/advanced/lifespan-events.mddocs_site/src/content/docs/advanced/openapi-callbacks.mddocs_site/src/content/docs/advanced/response-cookies-headers.mddocs_site/src/content/docs/advanced/response-directly.mddocs_site/src/content/docs/advanced/settings.mddocs_site/src/content/docs/advanced/sub-applications.mddocs_site/src/content/docs/advanced/templates.mddocs_site/src/content/docs/advanced/using-request-directly.mddocs_site/src/content/docs/advanced/websockets-advanced.mddocs_site/src/content/docs/how-to/background-task-patterns.mddocs_site/src/content/docs/how-to/circuit-breaker-recipes.mddocs_site/src/content/docs/how-to/configure-swagger-ui.mddocs_site/src/content/docs/how-to/custom-request-route.mddocs_site/src/content/docs/how-to/testing-database.md
▼docs_site/src/content/docs/advanced/additional-responses.md
+71-0
| @@ -0,0 +1,71 @@ | ||
1 | +--- | |
2 | +title: Additional Responses in OpenAPI | |
3 | +description: Document additional error responses and custom status codes in OpenAPI for JustAPI. | |
4 | +keywords: [JustAPI, OpenAPI, additional responses, error responses, documentation] | |
5 | +--- | |
6 | + | |
7 | +## Declaring Error Responses | |
8 | + | |
9 | +Document additional responses that your handler might return: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, HTTPException | |
13 | +from pydantic import BaseModel | |
14 | + | |
15 | +class ErrorResponse(BaseModel): | |
16 | + detail: str | |
17 | + | |
18 | +app = JustAPIApp() | |
19 | + | |
20 | +@app.get( | |
21 | + "/items/{item_id}", | |
22 | + responses={ | |
23 | + 404: {"model": ErrorResponse, "description": "Item not found"}, | |
24 | + 500: {"description": "Internal server error"}, | |
25 | + }, | |
26 | +) | |
27 | +def get_item(item_id: int): | |
28 | + if item_id == 0: | |
29 | + raise HTTPException(status_code=404, detail="Item not found") | |
30 | + return {"item_id": item_id} | |
31 | +``` | |
32 | + | |
33 | +## Multiple Response Models | |
34 | + | |
35 | +```python | |
36 | +@app.get( | |
37 | + "/users/{user_id}", | |
38 | + response_model=UserOut, | |
39 | + responses={ | |
40 | + 200: {"description": "User found"}, | |
41 | + 404: {"model": ErrorResponse, "description": "User not found"}, | |
42 | + 403: {"description": "Access denied"}, | |
43 | + }, | |
44 | +) | |
45 | +def get_user(user_id: int): | |
46 | + return {"user_id": user_id} | |
47 | +``` | |
48 | + | |
49 | +## Per-Media-Type Responses | |
50 | + | |
51 | +```python | |
52 | +@app.get( | |
53 | + "/data", | |
54 | + responses={ | |
55 | + 200: { | |
56 | + "content": { | |
57 | + "application/json": {"schema": {"type": "object"}}, | |
58 | + "text/plain": {"schema": {"type": "string"}}, | |
59 | + } | |
60 | + } | |
61 | + }, | |
62 | +) | |
63 | +def get_data(): | |
64 | + return {"data": "value"} | |
65 | +``` | |
66 | + | |
67 | +## See Also | |
68 | + | |
69 | +- [OpenAPI Callbacks & Webhooks](/advanced/openapi-callbacks/) — advanced OpenAPI features | |
70 | +- [Metadata & Docs URLs](/tutorials/metadata/) — app-level metadata | |
71 | +- [API Reference](/api-reference/) — full reference | |
0 | 72 | |
▼docs_site/src/content/docs/advanced/advanced-dependencies.md
+67-0
| @@ -0,0 +1,67 @@ | ||
1 | +--- | |
2 | +title: Advanced Dependencies | |
3 | +description: Parameterized dependencies, async generator dependencies, and dependency overrides in JustAPI. | |
4 | +keywords: [JustAPI, advanced dependencies, parameterized, overrides, async generator] | |
5 | +--- | |
6 | + | |
7 | +## Parameterized Dependencies | |
8 | + | |
9 | +Create dependencies that accept arguments: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Depends | |
13 | + | |
14 | +def get_query_filter(field: str, value: str): | |
15 | + def dependency(): | |
16 | + return {field: value} | |
17 | + return dependency | |
18 | + | |
19 | +app = JustAPIApp() | |
20 | + | |
21 | +@app.get("/items/") | |
22 | +def list_items(fresh: str = Depends(get_query_filter("status", "active"))): | |
23 | + return {"filter": fresh} | |
24 | +``` | |
25 | + | |
26 | +## Dependency Overrides | |
27 | + | |
28 | +Override dependencies for testing: | |
29 | + | |
30 | +```python | |
31 | +def get_db(): | |
32 | + return ProductionDB() | |
33 | + | |
34 | +app = JustAPIApp(dependencies=[Depends(get_db)]) | |
35 | + | |
36 | +# In tests | |
37 | +def override_get_db(): | |
38 | + return TestDB() | |
39 | + | |
40 | +app.dependency_overrides[get_db] = override_get_db | |
41 | + | |
42 | +def test_handler(): | |
43 | + response = client.get("/items/") | |
44 | + assert response.status == 200 | |
45 | +``` | |
46 | + | |
47 | +## Async Generator Dependencies | |
48 | + | |
49 | +```python | |
50 | +async def get_async_db(): | |
51 | + db = await create_async_connection() | |
52 | + try: | |
53 | + yield db | |
54 | + finally: | |
55 | + await db.close() | |
56 | + | |
57 | +@app.get("/items/") | |
58 | +async def list_items(db = Depends(get_async_db)): | |
59 | + items = await db.fetch_all("SELECT * FROM items") | |
60 | + return {"items": items} | |
61 | +``` | |
62 | + | |
63 | +## See Also | |
64 | + | |
65 | +- [Dependencies with yield](/tutorials/dependencies/dependencies-with-yield/) — cleanup patterns | |
66 | +- [Testing with Overrides](/advanced/async-tests/) — testing dependencies | |
67 | +- [Classes as Dependencies](/tutorials/dependencies/classes-as-dependencies/) — class-based deps | |
0 | 68 | |
▼docs_site/src/content/docs/advanced/advanced-middleware.md
+63-0
| @@ -0,0 +1,63 @@ | ||
1 | +--- | |
2 | +title: Advanced Middleware | |
3 | +description: Add ASGI middleware, HTTPS redirect, trusted hosts, and GZip compression in JustAPI. | |
4 | +keywords: [JustAPI, middleware, ASGI, HTTPS redirect, trusted host, GZip] | |
5 | +--- | |
6 | + | |
7 | +## Adding ASGI Middleware | |
8 | + | |
9 | +JustAPI supports any ASGI middleware: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp | |
13 | +from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware | |
14 | +from starlette.middleware.trustedhost import TrustedHostMiddleware | |
15 | + | |
16 | +app = JustAPIApp() | |
17 | + | |
18 | +app.add_middleware(HTTPSRedirectMiddleware) | |
19 | +``` | |
20 | + | |
21 | +## Trusted Host Middleware | |
22 | + | |
23 | +Guard against HTTP host header attacks: | |
24 | + | |
25 | +```python | |
26 | +app.add_middleware( | |
27 | + TrustedHostMiddleware, | |
28 | + allowed_hosts=["example.com", "*.example.com"], | |
29 | +) | |
30 | +``` | |
31 | + | |
32 | +## GZip Compression | |
33 | + | |
34 | +```python | |
35 | +from starlette.middleware.gzip import GZipMiddleware | |
36 | + | |
37 | +app.add_middleware(GZipMiddleware, minimum_size=500, compresslevel=9) | |
38 | +``` | |
39 | + | |
40 | +## Custom ASGI Middleware | |
41 | + | |
42 | +Write your own middleware class: | |
43 | + | |
44 | +```python | |
45 | +import time | |
46 | +from starlette.middleware.base import BaseHTTPMiddleware | |
47 | + | |
48 | +class TimingMiddleware(BaseHTTPMiddleware): | |
49 | + async def dispatch(self, request, call_next): | |
50 | + start = time.time() | |
51 | + response = await call_next(request) | |
52 | + duration = time.time() - start | |
53 | + response.headers["X-Process-Time"] = str(round(duration, 4)) | |
54 | + return response | |
55 | + | |
56 | +app.add_middleware(TimingMiddleware) | |
57 | +``` | |
58 | + | |
59 | +## See Also | |
60 | + | |
61 | +- [Middleware](/tutorials/middleware/) — basic middleware usage | |
62 | +- [Behind a Proxy](/advanced/behind-a-proxy/) — reverse proxy configuration | |
63 | +- [Sub Applications](/advanced/sub-applications/) — mounting sub-apps | |
0 | 64 | |
▼docs_site/src/content/docs/advanced/advanced-security.md
+76-0
| @@ -0,0 +1,76 @@ | ||
1 | +--- | |
2 | +title: Advanced Security | |
3 | +description: OAuth2 scopes, HTTP Basic Auth, and API key authentication in JustAPI. | |
4 | +keywords: [JustAPI, OAuth2 scopes, HTTP Basic Auth, API keys, advanced security] | |
5 | +--- | |
6 | + | |
7 | +## OAuth2 Scopes | |
8 | + | |
9 | +Control access with fine-grained permissions: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Security | |
13 | +from fastapi.security import OAuth2PasswordBearer | |
14 | + | |
15 | +oauth2_scheme = OAuth2PasswordBearer( | |
16 | + tokenUrl="token", | |
17 | + scopes={"read": "Read access", "write": "Write access", "admin": "Admin access"}, | |
18 | +) | |
19 | + | |
20 | +@app.get("/items/") | |
21 | +def list_items(token: str = Security(oauth2_scheme, scopes=["read"])): | |
22 | + return [] | |
23 | + | |
24 | +@app.post("/items/") | |
25 | +def create_item(token: str = Security(oauth2_scheme, scopes=["write"])): | |
26 | + return {} | |
27 | + | |
28 | +@app.delete("/items/{item_id}") | |
29 | +def delete_item(item_id: int, token: str = Security(oauth2_scheme, scopes=["admin"])): | |
30 | + return {"deleted": item_id} | |
31 | +``` | |
32 | + | |
33 | +## HTTP Basic Auth | |
34 | + | |
35 | +```python | |
36 | +from fastapi import Depends, HTTPException, status | |
37 | +from fastapi.security import HTTPBasic, HTTPBasicCredentials | |
38 | +import secrets | |
39 | + | |
40 | +security = HTTPBasic() | |
41 | + | |
42 | +def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)): | |
43 | + correct_username = secrets.compare_digest(credentials.username, "admin") | |
44 | + correct_password = secrets.compare_digest(credentials.password, "secret") | |
45 | + if not (correct_username and correct_password): | |
46 | + raise HTTPException(status_code=401, detail="Invalid credentials") | |
47 | + return credentials.username | |
48 | + | |
49 | +@app.get("/admin") | |
50 | +def admin_panel(username: str = Depends(verify_credentials)): | |
51 | + return {"message": f"Welcome {username}"} | |
52 | +``` | |
53 | + | |
54 | +## API Key Authentication | |
55 | + | |
56 | +```python | |
57 | +from fastapi import Security | |
58 | +from fastapi.security import APIKeyHeader | |
59 | + | |
60 | +api_key_header = APIKeyHeader(name="X-API-Key") | |
61 | + | |
62 | +def verify_api_key(api_key: str = Security(api_key_header)): | |
63 | + if api_key != "my-secret-key": | |
64 | + raise HTTPException(status_code=403, detail="Invalid API key") | |
65 | + return api_key | |
66 | + | |
67 | +@app.get("/data") | |
68 | +def get_data(key: str = Security(verify_api_key)): | |
69 | + return {"data": "sensitive"} | |
70 | +``` | |
71 | + | |
72 | +## See Also | |
73 | + | |
74 | +- [Security — First Steps](/tutorials/security/first-steps/) — basic security | |
75 | +- [OAuth2 + JWT](/tutorials/security/oauth2-jwt/) — JWT implementation | |
76 | +- [Secure Configuration](/security/secure-configuration/) — production security | |
0 | 77 | |
▼docs_site/src/content/docs/advanced/async-tests.md
+63-0
| @@ -0,0 +1,63 @@ | ||
1 | +--- | |
2 | +title: Async Tests | |
3 | +description: Write async test functions with httpx.AsyncClient for JustAPI applications. | |
4 | +keywords: [JustAPI, async tests, httpx, AsyncClient, pytest-asyncio] | |
5 | +--- | |
6 | + | |
7 | +## Async Test Client | |
8 | + | |
9 | +Use `httpx.AsyncClient` for async testing: | |
10 | + | |
11 | +```python | |
12 | +import pytest | |
13 | +import httpx | |
14 | +from justapi import JustAPIApp | |
15 | + | |
16 | +app = JustAPIApp() | |
17 | + | |
18 | +@app.get("/hello") | |
19 | +async def hello(): | |
20 | + return {"message": "Hello!"} | |
21 | + | |
22 | +@pytest.mark.anyio | |
23 | +async def test_hello(): | |
24 | + async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: | |
25 | + response = await client.get("/hello") | |
26 | + assert response.status_code == 200 | |
27 | + assert response.json() == {"message": "Hello!"} | |
28 | +``` | |
29 | + | |
30 | +## Testing with Dependencies | |
31 | + | |
32 | +```python | |
33 | +def override_get_db(): | |
34 | + return TestDB() | |
35 | + | |
36 | +app.dependency_overrides[get_db] = override_get_db | |
37 | + | |
38 | +@pytest.mark.anyio | |
39 | +async def test_with_override(): | |
40 | + async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: | |
41 | + response = await client.get("/items/") | |
42 | + assert response.status_code == 200 | |
43 | +``` | |
44 | + | |
45 | +## Setup and Teardown | |
46 | + | |
47 | +```python | |
48 | +@pytest.fixture | |
49 | +async def client(): | |
50 | + async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as c: | |
51 | + yield c | |
52 | + | |
53 | +@pytest.mark.anyio | |
54 | +async def test_example(client): | |
55 | + response = await client.get("/hello") | |
56 | + assert response.status_code == 200 | |
57 | +``` | |
58 | + | |
59 | +## See Also | |
60 | + | |
61 | +- [Testing](/tutorials/testing/) — sync testing with JustAPITestClient | |
62 | +- [Testing with Overrides](/advanced/async-tests/) — dependency overrides | |
63 | +- [Debugging](/tutorials/debugging/) — debugging techniques | |
0 | 64 | |
▼docs_site/src/content/docs/advanced/behind-a-proxy.md
+50-0
| @@ -0,0 +1,50 @@ | ||
1 | +--- | |
2 | +title: Behind a Proxy | |
3 | +description: Configure JustAPI to run behind a reverse proxy (Nginx, Traefik, HAProxy, Cloudflare). | |
4 | +keywords: [JustAPI, reverse proxy, Nginx, Traefik, root_path, X-Forwarded] | |
5 | +--- | |
6 | + | |
7 | +## root_path | |
8 | + | |
9 | +When running behind a proxy, set `root_path` so JustAPI knows the external path: | |
10 | + | |
11 | +```python | |
12 | +app = JustAPIApp(root_path="/api/v1") | |
13 | +``` | |
14 | + | |
15 | +## Nginx Configuration | |
16 | + | |
17 | +```nginx | |
18 | +location /api/ { | |
19 | + proxy_pass http://localhost:8000/; | |
20 | + proxy_set_header Host $host; | |
21 | + proxy_set_header X-Real-IP $remote_addr; | |
22 | + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | |
23 | + proxy_set_header X-Forwarded-Proto $scheme; | |
24 | +} | |
25 | +``` | |
26 | + | |
27 | +## Cloudflare | |
28 | + | |
29 | +Cloudflare automatically adds the right headers. JustAPI detects them: | |
30 | + | |
31 | +```python | |
32 | +app = JustAPIApp(root_path="/") | |
33 | +``` | |
34 | + | |
35 | +## Trusted Hosts with Proxy | |
36 | + | |
37 | +```python | |
38 | +from starlette.middleware.trustedhost import TrustedHostMiddleware | |
39 | + | |
40 | +app.add_middleware( | |
41 | + TrustedHostMiddleware, | |
42 | + allowed_hosts=["example.com", "*.example.com"], | |
43 | +) | |
44 | +``` | |
45 | + | |
46 | +## See Also | |
47 | + | |
48 | +- [Docker](/deployment/docker/) — containerized deployment | |
49 | +- [Kubernetes / Helm](/deployment/kubernetes-helm/) — K8s deployment | |
50 | +- [Production Checklist](/deployment/production-checklist/) — production setup | |
0 | 51 | |
▼docs_site/src/content/docs/advanced/custom-response.md
+74-0
| @@ -0,0 +1,74 @@ | ||
1 | +--- | |
2 | +title: Custom Response Classes | |
3 | +description: Use HTML, streaming, file, and other custom response types in JustAPI. | |
4 | +keywords: [JustAPI, custom response, HTML, streaming, file response, redirect] | |
5 | +--- | |
6 | + | |
7 | +## HTML Response | |
8 | + | |
9 | +```python | |
10 | +from starlette.responses import HTMLResponse | |
11 | +from justapi import JustAPIApp | |
12 | + | |
13 | +app = JustAPIApp() | |
14 | + | |
15 | +@app.get("/html") | |
16 | +def html_page(): | |
17 | + return HTMLResponse(content="<h1>Hello</h1><p>This is HTML</p>") | |
18 | +``` | |
19 | + | |
20 | +## Streaming Response | |
21 | + | |
22 | +```python | |
23 | +from starlette.responses import StreamingResponse | |
24 | +import asyncio | |
25 | + | |
26 | +async def generate_tokens(): | |
27 | + for i in range(10): | |
28 | + yield f"data: token_{i}\n\n" | |
29 | + await asyncio.sleep(0.1) | |
30 | + | |
31 | +@app.get("/stream") | |
32 | +def stream(): | |
33 | + return StreamingResponse(generate_tokens(), media_type="text/event-stream") | |
34 | +``` | |
35 | + | |
36 | +## File Response | |
37 | + | |
38 | +```python | |
39 | +from starlette.responses import FileResponse | |
40 | + | |
41 | +@app.get("/download") | |
42 | +def download(): | |
43 | + return FileResponse("report.pdf", filename="report.pdf") | |
44 | +``` | |
45 | + | |
46 | +## Plain Text Response | |
47 | + | |
48 | +```python | |
49 | +from starlette.responses import PlainTextResponse | |
50 | + | |
51 | +@app.get("/text") | |
52 | +def text(): | |
53 | + return PlainTextResponse("Hello, plain text!") | |
54 | +``` | |
55 | + | |
56 | +## Default Response Class | |
57 | + | |
58 | +Set a default response class for all routes: | |
59 | + | |
60 | +```python | |
61 | +from starlette.responses import ORJSONResponse | |
62 | + | |
63 | +app = JustAPIApp(default_response_class=ORJSONResponse) | |
64 | +``` | |
65 | + | |
66 | +:::tip | |
67 | +`ORJSONResponse` is faster than `JSONResponse` for large payloads. Install with `pip install orjson`. | |
68 | +::: | |
69 | + | |
70 | +## See Also | |
71 | + | |
72 | +- [Return a Response Directly](/advanced/response-directly/) — basic response object usage | |
73 | +- [Streaming Output](/advanced/streaming-output/) — JustAPI-native streaming | |
74 | +- [Static Files](/tutorials/static-files/) — serving files | |
0 | 75 | |
▼docs_site/src/content/docs/advanced/dataclasses.md
+47-0
| @@ -0,0 +1,47 @@ | ||
1 | +--- | |
2 | +title: Using Dataclasses | |
3 | +description: Use Python dataclasses as request models and response models in JustAPI. | |
4 | +keywords: [JustAPI, dataclasses, request model, Python dataclass] | |
5 | +--- | |
6 | + | |
7 | +## Dataclasses as Request Models | |
8 | + | |
9 | +JustAPI supports Python `dataclasses` as an alternative to Pydantic: | |
10 | + | |
11 | +```python | |
12 | +from dataclasses import dataclass | |
13 | +from typing import Optional | |
14 | +from justapi import JustAPIApp | |
15 | + | |
16 | +@dataclass | |
17 | +class Item: | |
18 | + name: str | |
19 | + price: float | |
20 | + description: Optional[str] = None | |
21 | + | |
22 | +app = JustAPIApp() | |
23 | + | |
24 | +@app.post("/items") | |
25 | +def create_item(item: Item): | |
26 | + return {"name": item.name, "price": item.price} | |
27 | +``` | |
28 | + | |
29 | +## Dataclasses as Response Models | |
30 | + | |
31 | +```python | |
32 | +@dataclass | |
33 | +class ItemResponse: | |
34 | + id: int | |
35 | + name: str | |
36 | + price: float | |
37 | + | |
38 | +@app.get("/items/{item_id}", response_model=ItemResponse) | |
39 | +def get_item(item_id: int): | |
40 | + return ItemResponse(id=item_id, name="Widget", price=9.99) | |
41 | +``` | |
42 | + | |
43 | +## See Also | |
44 | + | |
45 | +- [Request Body](/tutorials/request-body/) — Pydantic model usage | |
46 | +- [Using the Request Directly](/advanced/using-request-directly/) — raw request access | |
47 | +- [Response Model](/tutorials/response-model/) — filtering output | |
0 | 48 | |
▼docs_site/src/content/docs/advanced/generate-clients.md
+64-0
| @@ -0,0 +1,64 @@ | ||
1 | +--- | |
2 | +title: Generating SDKs | |
3 | +description: Generate client SDKs from your JustAPI OpenAPI schema for multiple languages. | |
4 | +keywords: [JustAPI, SDK generation, OpenAPI, client generation, code generation] | |
5 | +--- | |
6 | + | |
7 | +## Automatic OpenAPI Schema | |
8 | + | |
9 | +JustAPI generates an OpenAPI 3.1 schema at `/openapi.json`: | |
10 | + | |
11 | +```bash | |
12 | +curl http://localhost:8000/openapi.json | |
13 | +``` | |
14 | + | |
15 | +## Generate Python Client | |
16 | + | |
17 | +```bash | |
18 | +pip install openapi-generator-cli | |
19 | + | |
20 | +openapi-generator-cli generate \ | |
21 | + -i http://localhost:8000/openapi.json \ | |
22 | + -g python \ | |
23 | + -o ./clients/python | |
24 | +``` | |
25 | + | |
26 | +## Generate TypeScript Client | |
27 | + | |
28 | +```bash | |
29 | +openapi-generator-cli generate \ | |
30 | + -i http://localhost:8000/openapi.json \ | |
31 | + -g typescript-axios \ | |
32 | + -o ./clients/typescript | |
33 | +``` | |
34 | + | |
35 | +## Available Languages | |
36 | + | |
37 | +| Language | Generator | | |
38 | +|----------|-----------| | |
39 | +| Python | `python` | | |
40 | +| TypeScript | `typescript-axios` | | |
41 | +| Go | `go` | | |
42 | +| Java | `java` | | |
43 | +| Ruby | `ruby` | | |
44 | +| C# | `csharp` | | |
45 | +| Swift | `swift5` | | |
46 | + | |
47 | +## Custom ID Function | |
48 | + | |
49 | +Customize operation IDs for better SDK generation: | |
50 | + | |
51 | +```python | |
52 | +from justapi import JustAPIApp | |
53 | + | |
54 | +def custom_id(operation): | |
55 | + return f"{operation.tags[0]}_{operation.name}" if operation.tags else operation.name | |
56 | + | |
57 | +app = JustAPIApp(generate_unique_id_function=custom_id) | |
58 | +``` | |
59 | + | |
60 | +## See Also | |
61 | + | |
62 | +- [OpenAPI Callbacks & Webhooks](/advanced/openapi-callbacks/) — outbound schemas | |
63 | +- [Additional Responses in OpenAPI](/advanced/additional-responses/) — error docs | |
64 | +- [Metadata & Docs URLs](/tutorials/metadata/) — app metadata | |
0 | 65 | |
▼docs_site/src/content/docs/advanced/lifespan-events.md
+60-0
| @@ -0,0 +1,60 @@ | ||
1 | +--- | |
2 | +title: Lifespan Events | |
3 | +description: Run startup and shutdown code in JustAPI using the lifespan context manager. | |
4 | +keywords: [JustAPI, lifespan, startup, shutdown, events, lifecycle] | |
5 | +--- | |
6 | + | |
7 | +## Lifespan Context Manager | |
8 | + | |
9 | +Use `@asynccontextmanager` for startup/shutdown: | |
10 | + | |
11 | +```python | |
12 | +from contextlib import asynccontextmanager | |
13 | +from justapi import JustAPIApp | |
14 | + | |
15 | +@asynccontextmanager | |
16 | +async def lifespan(app): | |
17 | + # Startup: run before the server starts | |
18 | + print("Starting up...") | |
19 | + db = await create_connection() | |
20 | + app.state.db = db | |
21 | + yield | |
22 | + # Shutdown: run after the server stops | |
23 | + print("Shutting down...") | |
24 | + await db.close() | |
25 | + | |
26 | +app = JustAPIApp(lifespan=lifespan) | |
27 | +``` | |
28 | + | |
29 | +## Accessing State | |
30 | + | |
31 | +```python | |
32 | +@app.get("/health") | |
33 | +def health(request): | |
34 | + db = request.app.state.db | |
35 | + return {"status": "ok" if db else "error"} | |
36 | +``` | |
37 | + | |
38 | +## Multiple Resources | |
39 | + | |
40 | +```python | |
41 | +@asynccontextmanager | |
42 | +async def lifespan(app): | |
43 | + # Start all resources | |
44 | + app.state.db = await create_db() | |
45 | + app.state.cache = await create_cache() | |
46 | + app.state.queue = await create_queue() | |
47 | + yield | |
48 | + # Stop all resources | |
49 | + await app.state.queue.close() | |
50 | + await app.state.cache.close() | |
51 | + await app.state.db.close() | |
52 | + | |
53 | +app = JustAPIApp(lifespan=lifespan) | |
54 | +``` | |
55 | + | |
56 | +## See Also | |
57 | + | |
58 | +- [Settings & Environment Variables](/advanced/settings/) — configuration | |
59 | +- [Health Checks](/observability/health-checks/) — monitoring | |
60 | +- [SQL Databases](/tutorials/database-integration/) — database lifecycle | |
0 | 61 | |
▼docs_site/src/content/docs/advanced/openapi-callbacks.md
+57-0
| @@ -0,0 +1,57 @@ | ||
1 | +--- | |
2 | +title: OpenAPI Callbacks and Webhooks | |
3 | +description: Define OpenAPI callbacks and webhooks for outbound request schemas in JustAPI. | |
4 | +keywords: [JustAPI, OpenAPI, callbacks, webhooks, outbound requests] | |
5 | +--- | |
6 | + | |
7 | +## OpenAPI Callbacks | |
8 | + | |
9 | +Callbacks define the schema for outbound requests your API makes: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp | |
13 | +from pydantic import BaseModel | |
14 | + | |
15 | +class Order(BaseModel): | |
16 | + id: int | |
17 | + status: str | |
18 | + | |
19 | +app = JustAPIApp() | |
20 | + | |
21 | +@app.post("/orders", callbacks={ | |
22 | + "on_order_complete": { | |
23 | + "http://localhost:8000/webhook": { | |
24 | + "post": { | |
25 | + "requestBody": { | |
26 | + "content": { | |
27 | + "application/json": { | |
28 | + "schema": Order.model_jsonschema() | |
29 | + } | |
30 | + } | |
31 | + } | |
32 | + } | |
33 | + } | |
34 | + } | |
35 | +}) | |
36 | +def create_order(order: Order): | |
37 | + return {"id": 1, "status": "pending"} | |
38 | +``` | |
39 | + | |
40 | +## OpenAPI Webhooks | |
41 | + | |
42 | +Define outbound webhook schemas: | |
43 | + | |
44 | +```python | |
45 | +@app.webhooks.post("new-order") | |
46 | +def new_order_webhook(order: Order): | |
47 | + """ | |
48 | + This webhook is triggered when a new order is created. | |
49 | + """ | |
50 | + pass | |
51 | +``` | |
52 | + | |
53 | +## See Also | |
54 | + | |
55 | +- [Additional Responses in OpenAPI](/advanced/additional-responses/) — error responses | |
56 | +- [Metadata & Docs URLs](/tutorials/metadata/) — app metadata | |
57 | +- [OpenAPI Callbacks Reference](/advanced/openapi-callbacks/) — full reference | |
0 | 58 | |
▼docs_site/src/content/docs/advanced/response-cookies-headers.md
+57-0
| @@ -0,0 +1,57 @@ | ||
1 | +--- | |
2 | +title: Response Cookies and Headers | |
3 | +description: Set cookies and custom headers on responses in JustAPI. | |
4 | +keywords: [JustAPI, response cookies, response headers, Set-Cookie, custom headers] | |
5 | +--- | |
6 | + | |
7 | +## Setting Cookies | |
8 | + | |
9 | +```python | |
10 | +from starlette.responses import JSONResponse | |
11 | +from justapi import JustAPIApp | |
12 | + | |
13 | +app = JustAPIApp() | |
14 | + | |
15 | +@app.get("/set-cookie") | |
16 | +def set_cookie(): | |
17 | + response = JSONResponse(content={"message": "cookie set"}) | |
18 | + response.set_cookie("session_id", "abc123", httponly=True, secure=True) | |
19 | + return response | |
20 | +``` | |
21 | + | |
22 | +## Deleting Cookies | |
23 | + | |
24 | +```python | |
25 | +@app.get("/clear-cookie") | |
26 | +def clear_cookie(): | |
27 | + response = JSONResponse(content={"message": "cookie cleared"}) | |
28 | + response.delete_cookie("session_id") | |
29 | + return response | |
30 | +``` | |
31 | + | |
32 | +## Cookie Options | |
33 | + | |
34 | +| Option | Description | | |
35 | +|--------|-------------| | |
36 | +| `max_age` | Expiration in seconds | | |
37 | +| `httponly` | Prevent JavaScript access | | |
38 | +| `secure` | Only send over HTTPS | | |
39 | +| `samesite` | CSRF protection (`"lax"`, `"strict"`, `"none"`) | | |
40 | +| `path` | Cookie scope path | | |
41 | + | |
42 | +## Custom Response Headers | |
43 | + | |
44 | +```python | |
45 | +@app.get("/with-headers") | |
46 | +def with_headers(): | |
47 | + response = JSONResponse(content={"data": "value"}) | |
48 | + response.headers["X-Request-Id"] = "req-123" | |
49 | + response.headers["X-Response-Time"] = "42ms" | |
50 | + return response | |
51 | +``` | |
52 | + | |
53 | +## See Also | |
54 | + | |
55 | +- [Return a Response Directly](/advanced/response-directly/) — full response control | |
56 | +- [Security — First Steps](/tutorials/security/first-steps/) — auth with cookies | |
57 | +- [Cookie Parameters](/tutorials/cookie-params/) — reading cookies | |
0 | 58 | |
▼docs_site/src/content/docs/advanced/response-directly.md
+68-0
| @@ -0,0 +1,68 @@ | ||
1 | +--- | |
2 | +title: Return a Response Directly | |
3 | +description: Return a Response object directly from a JustAPI handler for full control over headers, status, and body. | |
4 | +keywords: [JustAPI, response, direct response, headers, status code] | |
5 | +--- | |
6 | + | |
7 | +## Returning a Response Object | |
8 | + | |
9 | +For full control over the response, return a `Response` object: | |
10 | + | |
11 | +```python | |
12 | +from starlette.responses import JSONResponse | |
13 | +from justapi import JustAPIApp | |
14 | + | |
15 | +app = JustAPIApp() | |
16 | + | |
17 | +@app.get("/custom") | |
18 | +def custom_response(): | |
19 | + return JSONResponse( | |
20 | + content={"message": "custom response"}, | |
21 | + status_code=202, | |
22 | + headers={"X-Custom-Header": "value"}, | |
23 | + ) | |
24 | +``` | |
25 | + | |
26 | +## Response Classes | |
27 | + | |
28 | +| Class | Use Case | | |
29 | +|-------|----------| | |
30 | +| `JSONResponse` | JSON response with custom headers | | |
31 | +| `HTMLResponse` | HTML content | | |
32 | +| `PlainTextResponse` | Plain text | | |
33 | +| `RedirectResponse` | Redirect to another URL | | |
34 | +| `StreamingResponse` | Streaming data | | |
35 | +| `FileResponse` | Serve a file | | |
36 | + | |
37 | +## Headers | |
38 | + | |
39 | +Set custom headers on any response: | |
40 | + | |
41 | +```python | |
42 | +@app.get("/with-headers") | |
43 | +def with_headers(): | |
44 | + return JSONResponse( | |
45 | + content={"data": "value"}, | |
46 | + headers={"X-Request-Id": "abc-123", "Cache-Control": "no-cache"}, | |
47 | + ) | |
48 | +``` | |
49 | + | |
50 | +## Redirect | |
51 | + | |
52 | +```python | |
53 | +from starlette.responses import RedirectResponse | |
54 | + | |
55 | +@app.get("/old-path") | |
56 | +def old_path(): | |
57 | + return RedirectResponse(url="/new-path") | |
58 | + | |
59 | +@app.get("/new-path") | |
60 | +def new_path(): | |
61 | + return {"message": "you are here"} | |
62 | +``` | |
63 | + | |
64 | +## See Also | |
65 | + | |
66 | +- [Custom Response Classes](/advanced/custom-response/) — HTML, stream, file responses | |
67 | +- [Response Status Code](/tutorials/response-status-code/) — status codes | |
68 | +- [Response Cookies & Headers](/advanced/response-cookies-headers/) — cookies and headers | |
0 | 69 | |
▼docs_site/src/content/docs/advanced/settings.md
+67-0
| @@ -0,0 +1,67 @@ | ||
1 | +--- | |
2 | +title: Settings and Environment Variables | |
3 | +description: Manage configuration with environment variables and Pydantic Settings in JustAPI. | |
4 | +keywords: [JustAPI, settings, environment variables, configuration, Pydantic Settings] | |
5 | +--- | |
6 | + | |
7 | +## Basic Settings with Pydantic | |
8 | + | |
9 | +```python | |
10 | +from pydantic_settings import BaseSettings | |
11 | + | |
12 | +class Settings(BaseSettings): | |
13 | + database_url: str = "sqlite:///app.db" | |
14 | + secret_key: str = "change-me" | |
15 | + debug: bool = False | |
16 | + api_prefix: str = "/api/v1" | |
17 | + | |
18 | + class Config: | |
19 | + env_file = ".env" | |
20 | + | |
21 | +settings = Settings() | |
22 | +``` | |
23 | + | |
24 | +## Using Settings in Your App | |
25 | + | |
26 | +```python | |
27 | +from justapi import JustAPIApp | |
28 | + | |
29 | +app = JustAPIApp(title="My App", debug=settings.debug) | |
30 | + | |
31 | +@app.get("/config") | |
32 | +def get_config(): | |
33 | + return { | |
34 | + "debug": settings.debug, | |
35 | + "api_prefix": settings.api_prefix, | |
36 | + } | |
37 | +``` | |
38 | + | |
39 | +## .env File | |
40 | + | |
41 | +```env | |
42 | +# .env | |
43 | +DATABASE_URL=postgresql://user:pass@localhost/db | |
44 | +SECRET_KEY=my-production-secret | |
45 | +DEBUG=false | |
46 | +API_PREFIX=/api/v1 | |
47 | +``` | |
48 | + | |
49 | +:::warning | |
50 | +Never commit `.env` files to version control. Add it to `.gitignore`. | |
51 | +::: | |
52 | + | |
53 | +## Accessing Environment Variables | |
54 | + | |
55 | +```python | |
56 | +import os | |
57 | + | |
58 | +# Standard library approach | |
59 | +debug = os.getenv("DEBUG", "false").lower() == "true" | |
60 | +database_url = os.getenv("DATABASE_URL", "sqlite:///app.db") | |
61 | +``` | |
62 | + | |
63 | +## See Also | |
64 | + | |
65 | +- [Lifespan Events](/advanced/lifespan-events/) — startup/shutdown | |
66 | +- [Configuration Reference](/reference/configuration/) — all config options | |
67 | +- [Deployment — Docker](/deployment/docker/) — environment in containers | |
0 | 68 | |
▼docs_site/src/content/docs/advanced/sub-applications.md
+60-0
| @@ -0,0 +1,60 @@ | ||
1 | +--- | |
2 | +title: Sub Applications — Mounts | |
3 | +description: Mount sub-applications and routers in JustAPI for modular application architecture. | |
4 | +keywords: [JustAPI, sub-applications, mounts, routers, modular, APIRouter] | |
5 | +--- | |
6 | + | |
7 | +## Using APIRouter | |
8 | + | |
9 | +Split your application into multiple files: | |
10 | + | |
11 | +```python | |
12 | +# routers/users.py | |
13 | +from justapi import APIRouter | |
14 | + | |
15 | +router = APIRouter(prefix="/users", tags=["users"]) | |
16 | + | |
17 | +@router.get("/") | |
18 | +def list_users(): | |
19 | + return [] | |
20 | + | |
21 | +@router.get("/{user_id}") | |
22 | +def get_user(user_id: int): | |
23 | + return {"user_id": user_id} | |
24 | +``` | |
25 | + | |
26 | +```python | |
27 | +# main.py | |
28 | +from justapi import JustAPIApp | |
29 | +from routers.users import router as users_router | |
30 | + | |
31 | +app = JustAPIApp() | |
32 | +app.include_router(users_router) | |
33 | +``` | |
34 | + | |
35 | +## Prefix and Tags | |
36 | + | |
37 | +```python | |
38 | +app.include_router(users_router, prefix="/api/v1", tags=["users"]) | |
39 | +``` | |
40 | + | |
41 | +All routes in `users_router` now start with `/api/v1/users/`. | |
42 | + | |
43 | +## Mounting Sub-Applications | |
44 | + | |
45 | +Mount an entire ASGI app at a sub-path: | |
46 | + | |
47 | +```python | |
48 | +from justapi import JustAPIApp | |
49 | +from starlette.applications import Starlette | |
50 | + | |
51 | +app = JustAPIApp() | |
52 | +sub_app = Starlette() | |
53 | +app.mount("/api/v2", sub_app) | |
54 | +``` | |
55 | + | |
56 | +## See Also | |
57 | + | |
58 | +- [Routing & Sub-routers](/tutorials/routing-subrouters/) — router basics | |
59 | +- [Bigger Applications](/tutorials/routing-subrouters/) — multi-file structure | |
60 | +- [Behind a Proxy](/advanced/behind-a-proxy/) — reverse proxy setup | |
0 | 61 | |
▼docs_site/src/content/docs/advanced/templates.md
+70-0
| @@ -0,0 +1,70 @@ | ||
1 | +--- | |
2 | +title: Templates (Jinja2) | |
3 | +description: Render Jinja2 templates for server-side HTML in JustAPI. | |
4 | +keywords: [JustAPI, templates, Jinja2, HTML, server-side rendering] | |
5 | +--- | |
6 | + | |
7 | +## Basic Template Rendering | |
8 | + | |
9 | +Install dependencies: | |
10 | + | |
11 | +```bash | |
12 | +pip install jinja2 | |
13 | +``` | |
14 | + | |
15 | +Create a template directory: | |
16 | + | |
17 | +``` | |
18 | +templates/ | |
19 | + index.html | |
20 | + items.html | |
21 | +``` | |
22 | + | |
23 | +Set up templates in your app: | |
24 | + | |
25 | +```python | |
26 | +from justapi import JustAPIApp | |
27 | +from starlette.templating import Jinja2Templates | |
28 | + | |
29 | +app = JustAPIApp() | |
30 | +templates = Jinja2Templates(directory="templates") | |
31 | + | |
32 | +@app.get("/") | |
33 | +def index(request): | |
34 | + return templates.TemplateResponse("index.html", {"request": request, "title": "Home"}) | |
35 | +``` | |
36 | + | |
37 | +## Template Example | |
38 | + | |
39 | +```html | |
40 | +<!-- templates/index.html --> | |
41 | +<!DOCTYPE html> | |
42 | +<html> | |
43 | +<head> | |
44 | + <title>{{ title }}</title> | |
45 | +</head> | |
46 | +<body> | |
47 | + <h1>{{ title }}</h1> | |
48 | + <p>Welcome to JustAPI</p> | |
49 | +</body> | |
50 | +</html> | |
51 | +``` | |
52 | + | |
53 | +## Passing Data | |
54 | + | |
55 | +```python | |
56 | +@app.get("/items") | |
57 | +def list_items(request): | |
58 | + items = [{"id": 1, "name": "Widget"}, {"id": 2, "name": "Gadget"}] | |
59 | + return templates.TemplateResponse("items.html", { | |
60 | + "request": request, | |
61 | + "items": items, | |
62 | + "title": "Items", | |
63 | + }) | |
64 | +``` | |
65 | + | |
66 | +## See Also | |
67 | + | |
68 | +- [Frontend](/tutorials/) — frontend integration | |
69 | +- [Static Files](/tutorials/static-files/) — serving CSS/JS | |
70 | +- [Custom Response Classes](/advanced/custom-response/) — HTML responses | |
0 | 71 | |
▼docs_site/src/content/docs/advanced/using-request-directly.md
+63-0
| @@ -0,0 +1,63 @@ | ||
1 | +--- | |
2 | +title: Using the Request Directly | |
3 | +description: Access the raw Starlette Request object for headers, body, and client info in JustAPI. | |
4 | +keywords: [JustAPI, request object, raw request, headers, body, client] | |
5 | +--- | |
6 | + | |
7 | +## Accessing the Request | |
8 | + | |
9 | +Any handler can receive the raw `Request` object as a parameter: | |
10 | + | |
11 | +```python | |
12 | +from starlette.requests import Request | |
13 | +from justapi import JustAPIApp | |
14 | + | |
15 | +app = JustAPIApp() | |
16 | + | |
17 | +@app.get("/info") | |
18 | +def get_info(request: Request): | |
19 | + return { | |
20 | + "method": request.method, | |
21 | + "url": str(request.url), | |
22 | + "client": request.client.host if request.client else None, | |
23 | + } | |
24 | +``` | |
25 | + | |
26 | +## Reading Headers | |
27 | + | |
28 | +```python | |
29 | +@app.get("/headers") | |
30 | +def get_headers(request: Request): | |
31 | + return { | |
32 | + "user_agent": request.headers.get("user-agent"), | |
33 | + "accept": request.headers.get("accept"), | |
34 | + "authorization": request.headers.get("authorization", "none"), | |
35 | + } | |
36 | +``` | |
37 | + | |
38 | +## Reading Body | |
39 | + | |
40 | +```python | |
41 | +@app.post("/raw-body") | |
42 | +async def raw_body(request: Request): | |
43 | + body = await request.body() | |
44 | + return {"size": len(body), "content": body.decode()} | |
45 | +``` | |
46 | + | |
47 | +## Path and Query Info | |
48 | + | |
49 | +```python | |
50 | +@app.get("/request-info") | |
51 | +def request_info(request: Request): | |
52 | + return { | |
53 | + "path": request.url.path, | |
54 | + "query": str(request.query_params), | |
55 | + "path_params": dict(request.path_params), | |
56 | + } | |
57 | +``` | |
58 | + | |
59 | +## See Also | |
60 | + | |
61 | +- [Path Parameters](/tutorials/path-params/) — extracting path params | |
62 | +- [Header Parameters](/tutorials/header-params/) — typed header access | |
63 | +- [Cookie Parameters](/tutorials/cookie-params/) — typed cookie access | |
0 | 64 | |
▼docs_site/src/content/docs/advanced/websockets-advanced.md
+68-0
| @@ -0,0 +1,68 @@ | ||
1 | +--- | |
2 | +title: WebSockets Advanced | |
3 | +description: Advanced WebSocket patterns — connection management, broadcasting, and testing in JustAPI. | |
4 | +keywords: [JustAPI, WebSocket, advanced, broadcasting, connection management] | |
5 | +--- | |
6 | + | |
7 | +## WebSocket with Connection Manager | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp | |
11 | +from starlette.websockets import WebSocket, WebSocketDisconnect | |
12 | + | |
13 | +app = JustAPIApp() | |
14 | + | |
15 | +class ConnectionManager: | |
16 | + def __init__(self): | |
17 | + self.connections: list[WebSocket] = [] | |
18 | + | |
19 | + async def connect(self, websocket: WebSocket): | |
20 | + await websocket.accept() | |
21 | + self.connections.append(websocket) | |
22 | + | |
23 | + def disconnect(self, websocket: WebSocket): | |
24 | + self.connections.remove(websocket) | |
25 | + | |
26 | + async def broadcast(self, message: str): | |
27 | + for conn in self.connections: | |
28 | + await conn.send_text(message) | |
29 | + | |
30 | +manager = ConnectionManager() | |
31 | + | |
32 | +@app.websocket("/ws") | |
33 | +async def websocket_endpoint(websocket: WebSocket): | |
34 | + await manager.connect(websocket) | |
35 | + try: | |
36 | + while True: | |
37 | + data = await websocket.receive_text() | |
38 | + await manager.broadcast(f"Echo: {data}") | |
39 | + except WebSocketDisconnect: | |
40 | + manager.disconnect(websocket) | |
41 | +``` | |
42 | + | |
43 | +## Private WebSocket Rooms | |
44 | + | |
45 | +```python | |
46 | +from collections import defaultdict | |
47 | + | |
48 | +rooms: dict[str, list[WebSocket]] = defaultdict(list) | |
49 | + | |
50 | +@app.websocket("/ws/{room}") | |
51 | +async def room_websocket(websocket: WebSocket, room: str): | |
52 | + await websocket.accept() | |
53 | + rooms[room].append(websocket) | |
54 | + try: | |
55 | + while True: | |
56 | + data = await websocket.receive_text() | |
57 | + for conn in rooms[room]: | |
58 | + if conn != websocket: | |
59 | + await conn.send_text(data) | |
60 | + except WebSocketDisconnect: | |
61 | + rooms[room].remove(websocket) | |
62 | +``` | |
63 | + | |
64 | +## See Also | |
65 | + | |
66 | +- [WebSockets & SSE](/tutorials/websockets-sse/) — basic WebSocket usage | |
67 | +- [Streaming Output](/advanced/streaming-output/) — SSE streaming | |
68 | +- [Resilience Patterns](/advanced/resilience-patterns/) — connection management | |
0 | 69 | |
▼docs_site/src/content/docs/how-to/background-task-patterns.md
+79-0
| @@ -0,0 +1,79 @@ | ||
1 | +--- | |
2 | +title: Background Task Patterns | |
3 | +description: Implement prioritized tasks, progress tracking, and error handling with JustAPI background tasks. | |
4 | +keywords: [JustAPI, background tasks, patterns, priority, progress, error handling] | |
5 | +--- | |
6 | + | |
7 | +## Basic Background Tasks | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp, BackgroundTasks | |
11 | + | |
12 | +app = JustAPIApp() | |
13 | + | |
14 | +def process_data(task_id: str, data: dict): | |
15 | + # Long-running processing | |
16 | + import time | |
17 | + time.sleep(5) | |
18 | + # Save results | |
19 | + print(f"Task {task_id} completed") | |
20 | + | |
21 | +@app.post("/process") | |
22 | +def start_processing(data: dict, background_tasks: BackgroundTasks): | |
23 | + task_id = "task-123" | |
24 | + background_tasks.add_task(process_data, task_id, data) | |
25 | + return {"task_id": task_id, "status": "started"} | |
26 | +``` | |
27 | + | |
28 | +## Progress Tracking | |
29 | + | |
30 | +```python | |
31 | +import asyncio | |
32 | + | |
33 | +task_progress = {} | |
34 | + | |
35 | +def update_progress(task_id: str, progress: int, status: str = "running"): | |
36 | + task_progress[task_id] = {"progress": progress, "status": status} | |
37 | + | |
38 | +def long_task(task_id: str): | |
39 | + for i in range(100): | |
40 | + time.sleep(0.1) | |
41 | + update_progress(task_id, i + 1) | |
42 | + update_progress(task_id, 100, "completed") | |
43 | + | |
44 | +@app.post("/tasks") | |
45 | +def create_task(background_tasks: BackgroundTasks): | |
46 | + import uuid | |
47 | + task_id = str(uuid.uuid4()) | |
48 | + update_progress(task_id, 0, "pending") | |
49 | + background_tasks.add_task(long_task, task_id) | |
50 | + return {"task_id": task_id} | |
51 | + | |
52 | +@app.get("/tasks/{task_id}") | |
53 | +def get_task_progress(task_id: str): | |
54 | + return task_progress.get(task_id, {"error": "task not found"}) | |
55 | +``` | |
56 | + | |
57 | +## Error Handling in Tasks | |
58 | + | |
59 | +```python | |
60 | +def safe_task(task_id: str): | |
61 | + try: | |
62 | + # Risky operation | |
63 | + risky_operation() | |
64 | + update_progress(task_id, 100, "completed") | |
65 | + except Exception as e: | |
66 | + update_progress(task_id, 0, f"failed: {str(e)}") | |
67 | + | |
68 | +@app.post("/safe-tasks") | |
69 | +def create_safe_task(background_tasks: BackgroundTasks): | |
70 | + task_id = "safe-task-1" | |
71 | + background_tasks.add_task(safe_task, task_id) | |
72 | + return {"task_id": task_id} | |
73 | +``` | |
74 | + | |
75 | +## See Also | |
76 | + | |
77 | +- [Background Tasks](/tutorials/background-tasks/) — basic background tasks | |
78 | +- [Lifespan Events](/advanced/lifespan-events/) — startup/shutdown | |
79 | +- [Settings & Environment Variables](/advanced/settings/) — configuration | |
0 | 80 | |
▼docs_site/src/content/docs/how-to/circuit-breaker-recipes.md
+118-0
| @@ -0,0 +1,118 @@ | ||
1 | +--- | |
2 | +title: Circuit Breaker Recipes | |
3 | +description: Configure circuit breakers, retry policies, and bulkheads for real-world scenarios in JustAPI. | |
4 | +keywords: [JustAPI, circuit breaker, retry, bulkhead, resilience, patterns] | |
5 | +--- | |
6 | + | |
7 | +## Basic Circuit Breaker | |
8 | + | |
9 | +```python | |
10 | +import time | |
11 | +from justapi import JustAPIApp | |
12 | + | |
13 | +class CircuitBreaker: | |
14 | + def __init__(self, failure_threshold=3, recovery_timeout=60): | |
15 | + self.failure_count = 0 | |
16 | + self.failure_threshold = failure_threshold | |
17 | + self.recovery_timeout = recovery_timeout | |
18 | + self.state = "closed" | |
19 | + self.last_failure_time = None | |
20 | + | |
21 | + def call(self, func, *args, **kwargs): | |
22 | + if self.state == "open": | |
23 | + if time.time() - self.last_failure_time > self.recovery_timeout: | |
24 | + self.state = "half-open" | |
25 | + else: | |
26 | + raise Exception("Circuit breaker is open") | |
27 | + | |
28 | + try: | |
29 | + result = func(*args, **kwargs) | |
30 | + self.failure_count = 0 | |
31 | + self.state = "closed" | |
32 | + return result | |
33 | + except Exception as e: | |
34 | + self.failure_count += 1 | |
35 | + self.last_failure_time = time.time() | |
36 | + if self.failure_count >= self.failure_threshold: | |
37 | + self.state = "open" | |
38 | + raise | |
39 | + | |
40 | +breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=60) | |
41 | + | |
42 | +def unreliable_service(): | |
43 | + # Simulate a failing service | |
44 | + import random | |
45 | + if random.random() < 0.5: | |
46 | + raise Exception("Service unavailable") | |
47 | + return {"status": "ok"} | |
48 | + | |
49 | +@app.get("/data") | |
50 | +def get_data(): | |
51 | + return breaker.call(unreliable_service) | |
52 | +``` | |
53 | + | |
54 | +## Retry with Backoff | |
55 | + | |
56 | +```python | |
57 | +import asyncio | |
58 | +from justapi import JustAPIApp | |
59 | + | |
60 | +async def retry_with_backoff(func, max_retries=3, base_delay=1): | |
61 | + for attempt in range(max_retries): | |
62 | + try: | |
63 | + return await func() | |
64 | + except Exception as e: | |
65 | + if attempt == max_retries - 1: | |
66 | + raise | |
67 | + await asyncio.sleep(base_delay * (2 ** attempt)) | |
68 | + | |
69 | +@app.get("/retry") | |
70 | +async def retry_example(): | |
71 | + async def call_external(): | |
72 | + # Simulate external call | |
73 | + import random | |
74 | + if random.random() < 0.7: | |
75 | + raise Exception("Temporary failure") | |
76 | + return {"data": "success"} | |
77 | + | |
78 | + return await retry_with_backoff(call_external) | |
79 | +``` | |
80 | + | |
81 | +## Rate Limiting | |
82 | + | |
83 | +```python | |
84 | +import time | |
85 | +from collections import defaultdict | |
86 | + | |
87 | +class RateLimiter: | |
88 | + def __init__(self, max_requests=100, window=60): | |
89 | + self.max_requests = max_requests | |
90 | + self.window = window | |
91 | + self.requests = defaultdict(list) | |
92 | + | |
93 | + def is_allowed(self, client_id): | |
94 | + now = time.time() | |
95 | + self.requests[client_id] = [ | |
96 | + t for t in self.requests[client_id] if now - t < self.window | |
97 | + ] | |
98 | + if len(self.requests[client_id]) >= self.max_requests: | |
99 | + return False | |
100 | + self.requests[client_id].append(now) | |
101 | + return True | |
102 | + | |
103 | +limiter = RateLimiter(max_requests=100, window=60) | |
104 | + | |
105 | +@app.get("/api/data") | |
106 | +def rate_limited_data(request): | |
107 | + client_id = request.client.host | |
108 | + if not limiter.is_allowed(client_id): | |
109 | + from starlette.responses import JSONResponse | |
110 | + return JSONResponse(content={"error": "Rate limit exceeded"}, status_code=429) | |
111 | + return {"data": "ok"} | |
112 | +``` | |
113 | + | |
114 | +## See Also | |
115 | + | |
116 | +- [Resilience Patterns](/advanced/resilience-patterns/) — full resilience guide | |
117 | +- [Rate Limiting](/tutorials/middleware/) — built-in rate limiting | |
118 | +- [Error Handling](/tutorials/error-handling/) — exception handling | |
0 | 119 | |
▼docs_site/src/content/docs/how-to/configure-swagger-ui.md
+50-0
| @@ -0,0 +1,50 @@ | ||
1 | +--- | |
2 | +title: Configure Swagger UI | |
3 | +description: Customize the Swagger UI appearance and behavior in JustAPI. | |
4 | +keywords: [JustAPI, Swagger UI, documentation, OpenAPI, customization] | |
5 | +--- | |
6 | + | |
7 | +## Basic Configuration | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp | |
11 | + | |
12 | +app = JustAPIApp( | |
13 | + docs_url="/docs", | |
14 | + redoc_url="/redoc", | |
15 | + swagger_ui_parameters={ | |
16 | + "docExpansion": "none", | |
17 | + "filter": True, | |
18 | + "tagsSorter": "alpha", | |
19 | + "operationsSorter": "alpha", | |
20 | + }, | |
21 | +) | |
22 | +``` | |
23 | + | |
24 | +## Common Options | |
25 | + | |
26 | +| Parameter | Type | Description | | |
27 | +|-----------|------|-------------| | |
28 | +| `docExpansion` | `str` | `"none"`, `"list"`, `"full"` | | |
29 | +| `filter` | `bool` or `str` | Enable search filter | | |
30 | +| `tagsSorter` | `str` | `"alpha"` for alphabetical | | |
31 | +| `operationsSorter` | `str` | `"alpha"` for alphabetical | | |
32 | +| `defaultModelsExpandDepth` | `int` | Model expansion depth | | |
33 | +| `defaultModelExpandDepth` | `int` | Individual model depth | | |
34 | + | |
35 | +## Disable Docs in Production | |
36 | + | |
37 | +```python | |
38 | +import os | |
39 | + | |
40 | +app = JustAPIApp( | |
41 | + docs_url="/docs" if os.getenv("ENVIRONMENT") == "development" else None, | |
42 | + redoc_url="/redoc" if os.getenv("ENVIRONMENT") == "development" else None, | |
43 | +) | |
44 | +``` | |
45 | + | |
46 | +## See Also | |
47 | + | |
48 | +- [Additional Responses in OpenAPI](/advanced/additional-responses/) — error docs | |
49 | +- [Metadata & Docs URLs](/tutorials/metadata/) — app metadata | |
50 | +- [OpenAPI Callbacks & Webhooks](/advanced/openapi-callbacks/) — outbound schemas | |
0 | 51 | |
▼docs_site/src/content/docs/how-to/custom-request-route.md
+57-0
| @@ -0,0 +1,57 @@ | ||
1 | +--- | |
2 | +title: Custom Request and Route Classes | |
3 | +description: Extend the Request and APIRoute classes in JustAPI for custom behavior. | |
4 | +keywords: [JustAPI, custom request, custom route, APIRoute, extend] | |
5 | +--- | |
6 | + | |
7 | +## Custom Route Class | |
8 | + | |
9 | +Override the default `APIRoute` to add custom behavior: | |
10 | + | |
11 | +```python | |
12 | +from starlette.routing import APIRoute | |
13 | +from justapi import JustAPIApp, Request | |
14 | + | |
15 | +class TimingRoute(APIRoute): | |
16 | + def get_route_handler(self): | |
17 | + original_handler = super().get_route_handler() | |
18 | + | |
19 | + async def custom_handler(request: Request): | |
20 | + import time | |
21 | + start = time.time() | |
22 | + response = await original_handler(request) | |
23 | + duration = time.time() - start | |
24 | + response.headers["X-Process-Time"] = str(round(duration, 4)) | |
25 | + return response | |
26 | + | |
27 | + return custom_handler | |
28 | + | |
29 | +app = JustAPIApp(route_class=TimingRoute) | |
30 | + | |
31 | +@app.get("/items/") | |
32 | +def list_items(): | |
33 | + return {"items": []} | |
34 | +``` | |
35 | + | |
36 | +## Custom Request Class | |
37 | + | |
38 | +```python | |
39 | +from starlette.requests import Request | |
40 | + | |
41 | +class CustomRequest(Request): | |
42 | + async def json(self): | |
43 | + body = await super().json() | |
44 | + # Transform the body before validation | |
45 | + return body | |
46 | + | |
47 | +@app.post("/items") | |
48 | +async def create_item(request: CustomRequest): | |
49 | + data = await request.json() | |
50 | + return data | |
51 | +``` | |
52 | + | |
53 | +## See Also | |
54 | + | |
55 | +- [Using the Request Directly](/advanced/using-request-directly/) — raw request access | |
56 | +- [Advanced Middleware](/advanced/advanced-middleware/) — custom middleware | |
57 | +- [Advanced Dependencies](/advanced/advanced-dependencies/) — custom deps | |
0 | 58 | |
▼docs_site/src/content/docs/how-to/testing-database.md
+80-0
| @@ -0,0 +1,80 @@ | ||
1 | +--- | |
2 | +title: Testing a Database | |
3 | +description: Use in-memory SQLite and transaction rollback for fast database tests in JustAPI. | |
4 | +keywords: [JustAPI, testing, database, SQLite, in-memory, transactions] | |
5 | +--- | |
6 | + | |
7 | +## In-Memory SQLite | |
8 | + | |
9 | +Use in-memory SQLite for fast, isolated tests: | |
10 | + | |
11 | +```python | |
12 | +import sqlite3 | |
13 | +from justapi import JustAPIApp, JustAPITestClient | |
14 | + | |
15 | +def get_test_db(): | |
16 | + conn = sqlite3.connect(":memory:") | |
17 | + conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)") | |
18 | + return conn | |
19 | + | |
20 | +app = JustAPIApp() | |
21 | + | |
22 | +@app.get("/items") | |
23 | +def list_items(): | |
24 | + db = get_test_db() | |
25 | + cursor = db.execute("SELECT * FROM items") | |
26 | + return {"items": cursor.fetchall()} | |
27 | + | |
28 | +def test_list_items_empty(): | |
29 | + client = JustAPITestClient(app) | |
30 | + response = client.get("/items") | |
31 | + assert response.status_code == 200 | |
32 | + assert response.json() == {"items": []} | |
33 | +``` | |
34 | + | |
35 | +## Transaction Rollback | |
36 | + | |
37 | +Wrap each test in a transaction and rollback: | |
38 | + | |
39 | +```python | |
40 | +import pytest | |
41 | + | |
42 | +@pytest.fixture | |
43 | +def db(): | |
44 | + conn = sqlite3.connect(":memory:") | |
45 | + conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)") | |
46 | + yield conn | |
47 | + conn.rollback() # undo all changes | |
48 | + conn.close() | |
49 | + | |
50 | +def test_create_item(db): | |
51 | + db.execute("INSERT INTO items (name) VALUES (?)", ("Widget",)) | |
52 | + cursor = db.execute("SELECT * FROM items") | |
53 | + assert cursor.fetchone() is not None | |
54 | +``` | |
55 | + | |
56 | +## Dependency Override for Testing | |
57 | + | |
58 | +```python | |
59 | +from justapi import Depends | |
60 | + | |
61 | +def get_db(): | |
62 | + return production_db | |
63 | + | |
64 | +def override_get_db(): | |
65 | + return sqlite3.connect(":memory:") | |
66 | + | |
67 | +app = JustAPIApp() | |
68 | +app.dependency_overrides[get_db] = override_get_db | |
69 | + | |
70 | +def test_handler(): | |
71 | + client = JustAPITestClient(app) | |
72 | + response = client.get("/items/") | |
73 | + assert response.status_code == 200 | |
74 | +``` | |
75 | + | |
76 | +## See Also | |
77 | + | |
78 | +- [Testing](/tutorials/testing/) — basic testing | |
79 | +- [SQL Databases](/tutorials/database-integration/) — database setup | |
80 | +- [Advanced Dependencies](/advanced/advanced-dependencies/) — dependency overrides | |
0 | 81 | |