1697b71
commit 1697b71
11 files changed
+758 -48
Showing 11 changed files+758-48
docs_site/src/content/docs/api-reference/auth.mddocs_site/src/content/docs/api-reference/controller.mddocs_site/src/content/docs/api-reference/dag.mddocs_site/src/content/docs/api-reference/mailer.mddocs_site/src/content/docs/api-reference/rate-limiter.mddocs_site/src/content/docs/api-reference/session.mddocs_site/src/content/docs/api-reference/system-routes.mddocs_site/src/content/docs/reference/environment-variables.mddocs_site/src/content/docs/tutorials/frontend.mddocs_site/src/content/docs/tutorials/stream-json-lines.mddocs_site/src/content/docs/tutorials/testing-utilities.md
▼docs_site/src/content/docs/api-reference/auth.md
+105-0
| @@ -0,0 +1,105 @@ | ||
1 | +--- | |
2 | +title: Authentication | |
3 | +description: JWT, OAuth2, and API key authentication in JustAPI — Rust-native and Python-compatible. | |
4 | +keywords: [JustAPI, auth, JWT, OAuth2, API key, authentication, security] | |
5 | +--- | |
6 | + | |
7 | +## JwtAuth (Rust-native) | |
8 | + | |
9 | +Fast, Rust-native JWT encoding/decoding: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, JwtAuth, Security | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +jwt = JwtAuth(secret="your-secret-key", algorithm="HS256") | |
17 | + | |
18 | +@app.get("/protected") | |
19 | +def protected(token: dict = Security(jwt)): | |
20 | + return {"user": token["sub"]} | |
21 | +``` | |
22 | + | |
23 | +### JwtAuth Options | |
24 | + | |
25 | +| Parameter | Type | Default | Description | | |
26 | +|-----------|------|---------|-------------| | |
27 | +| `secret` | `str` | — | Signing secret | | |
28 | +| `algorithm` | `str` | `"HS256"` | JWT algorithm | | |
29 | +| `auto_error` | `bool` | `True` | Raise 401 on invalid token | | |
30 | + | |
31 | +### Supported Algorithms | |
32 | + | |
33 | +HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ED25519 | |
34 | + | |
35 | +### Encode/Decode | |
36 | + | |
37 | +```python | |
38 | +# Encode | |
39 | +token = jwt.encode({"sub": "alice", "role": "admin"}) | |
40 | + | |
41 | +# Decode | |
42 | +claims = jwt.decode(token, verify_exp=True, verify_iat=True) | |
43 | +``` | |
44 | + | |
45 | +### Decode Options | |
46 | + | |
47 | +| Option | Type | Description | | |
48 | +|--------|------|-------------| | |
49 | +| `verify_exp` | `bool` | Verify expiration claim | | |
50 | +| `verify_iat` | `bool` | Verify issued-at claim | | |
51 | +| `verify_nbf` | `bool` | Verify not-before claim | | |
52 | +| `verify_aud` | `bool` | Verify audience claim | | |
53 | +| `verify_iss` | `bool` | Verify issuer claim | | |
54 | + | |
55 | +## OAuth2PasswordBearer | |
56 | + | |
57 | +FastAPI-compatible bearer token extractor: | |
58 | + | |
59 | +```python | |
60 | +from justapi import OAuth2PasswordBearer | |
61 | + | |
62 | +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") | |
63 | + | |
64 | +@app.get("/users/me") | |
65 | +def read_me(token: str = Security(oauth2_scheme)): | |
66 | + return {"token": token} | |
67 | +``` | |
68 | + | |
69 | +## OAuth2PasswordRequestForm | |
70 | + | |
71 | +Form dependency for token endpoints: | |
72 | + | |
73 | +```python | |
74 | +from justapi import OAuth2PasswordRequestForm | |
75 | + | |
76 | +@app.post("/token") | |
77 | +def login(form: OAuth2PasswordRequestForm): | |
78 | + # form.username, form.password, form.scope | |
79 | + return {"access_token": form.username, "token_type": "bearer"} | |
80 | +``` | |
81 | + | |
82 | +### Strict Variant | |
83 | + | |
84 | +```python | |
85 | +from justapi import OAuth2PasswordRequestFormStrict | |
86 | + | |
87 | +# Requires grant_type=password in the form | |
88 | +@app.post("/token") | |
89 | +def login(form: OAuth2PasswordRequestFormStrict): | |
90 | + ... | |
91 | +``` | |
92 | + | |
93 | +## Set JWT Auth (Convenience) | |
94 | + | |
95 | +```python | |
96 | +app.set_jwt_auth(secret="my-secret", algorithm="HS256") | |
97 | +``` | |
98 | + | |
99 | +This adds Rust-native JWT middleware to all routes. | |
100 | + | |
101 | +## See Also | |
102 | + | |
103 | +- [Security — First Steps](/tutorials/security/first-steps/) — basic auth setup | |
104 | +- [OAuth2 + JWT](/tutorials/security/oauth2-jwt/) — production JWT | |
105 | +- [Advanced Security](/advanced/advanced-security/) — scopes, API keys | |
0 | 106 | |
▼docs_site/src/content/docs/api-reference/controller.md
+66-0
| @@ -0,0 +1,66 @@ | ||
1 | +--- | |
2 | +title: Controller | |
3 | +description: Class-based route controllers in JustAPI — organize related routes in a single class. | |
4 | +keywords: [JustAPI, controller, class-based routes, route decorators] | |
5 | +--- | |
6 | + | |
7 | +## Basic Controller | |
8 | + | |
9 | +Use `@controller` and `@route_get/post/etc` to group related routes in a class: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Controller, controller, route_get, route_post | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +@controller("/users") | |
17 | +class UserController: | |
18 | + @route_get("/") | |
19 | + def list_users(self): | |
20 | + return [] | |
21 | + | |
22 | + @route_get("/{user_id}") | |
23 | + def get_user(self, user_id: int): | |
24 | + return {"user_id": user_id} | |
25 | + | |
26 | + @route_post("/") | |
27 | + def create_user(self, body: dict): | |
28 | + return body | |
29 | + | |
30 | +app.include_controller(UserController) | |
31 | +``` | |
32 | + | |
33 | +All routes are prefixed with `/users`. | |
34 | + | |
35 | +## Route Decorators | |
36 | + | |
37 | +| Decorator | HTTP Method | | |
38 | +|-----------|-------------| | |
39 | +| `@route_get(path)` | GET | | |
40 | +| `@route_post(path)` | POST | | |
41 | +| `@route_put(path)` | PUT | | |
42 | +| `@route_patch(path)` | PATCH | | |
43 | +| `@route_delete(path)` | DELETE | | |
44 | +| `@route_query(path)` | QUERY (RFC 10008) | | |
45 | +| `@route_sse(path)` | GET (SSE stream) | | |
46 | +| `@route_websocket(path)` | WebSocket | | |
47 | + | |
48 | +## Controller with Dependencies | |
49 | + | |
50 | +```python | |
51 | +from justapi import Depends, Security | |
52 | + | |
53 | +def verify_auth(token: str = Security(oauth2_scheme)): | |
54 | + return {"user_id": 1} | |
55 | + | |
56 | +@controller("/admin", dependencies=[Depends(verify_auth)]) | |
57 | +class AdminController: | |
58 | + @route_get("/dashboard") | |
59 | + def dashboard(self): | |
60 | + return {"message": "admin only"} | |
61 | +``` | |
62 | + | |
63 | +## See Also | |
64 | + | |
65 | +- [Routing & Sub-routers](/tutorials/routing-subrouters/) — APIRouter usage | |
66 | +- [APIRouter](/api-reference/apirouter/) — router reference | |
0 | 67 | |
▼docs_site/src/content/docs/api-reference/dag.md
+57-0
| @@ -0,0 +1,57 @@ | ||
1 | +--- | |
2 | +title: Dag and DagNode | |
3 | +description: Execute tasks in a directed acyclic graph (DAG) with dependency resolution in JustAPI. | |
4 | +keywords: [JustAPI, DAG, directed acyclic graph, task orchestration, dependencies] | |
5 | +--- | |
6 | + | |
7 | +## Basic Usage | |
8 | + | |
9 | +Define tasks as nodes and let JustAPI execute them in dependency order: | |
10 | + | |
11 | +```python | |
12 | +from justapi import Dag, DagNode | |
13 | + | |
14 | +# Define tasks | |
15 | +def fetch_data(): | |
16 | + return {"raw": [1, 2, 3]} | |
17 | + | |
18 | +def transform(data): | |
19 | + return {"transformed": [x * 2 for x in data["raw"]]} | |
20 | + | |
21 | +def save(data): | |
22 | + print(f"Saving: {data}") | |
23 | + return {"status": "saved"} | |
24 | + | |
25 | +# Build DAG | |
26 | +dag = Dag([ | |
27 | + DagNode("fetch", fetch_data), | |
28 | + DagNode("transform", transform, dependencies=["fetch"]), | |
29 | + DagNode("save", save, dependencies=["transform"]), | |
30 | +]) | |
31 | + | |
32 | +# Execute | |
33 | +result = dag.execute({"input": "test"}) | |
34 | +# Runs: fetch → transform → save | |
35 | +``` | |
36 | + | |
37 | +## DagNode | |
38 | + | |
39 | +| Parameter | Type | Description | | |
40 | +|-----------|------|-------------| | |
41 | +| `name` | `str` | Unique node identifier | | |
42 | +| `handler` | `Callable` | Function to execute | | |
43 | +| `dependencies` | `list[str]` | Names of nodes this depends on | | |
44 | + | |
45 | +## Dag.execute | |
46 | + | |
47 | +```python | |
48 | +result = dag.execute(inputs={}) | |
49 | +# Returns: asyncio.Future with final state dict | |
50 | +``` | |
51 | + | |
52 | +The `inputs` dict is the initial state. Each node's return value is merged into the state under its `name` key. Dependent nodes receive the accumulated state. | |
53 | + | |
54 | +## See Also | |
55 | + | |
56 | +- [Background Tasks](/tutorials/background-tasks/) — post-response tasks | |
57 | +- [Resilience Patterns](/advanced/resilience-patterns/) — error handling in DAGs | |
0 | 58 | |
▼docs_site/src/content/docs/api-reference/mailer.md
+86-0
| @@ -0,0 +1,86 @@ | ||
1 | +--- | |
2 | +title: Mailer | |
3 | +description: Send emails from JustAPI applications — sync, async, and template-based. | |
4 | +keywords: [JustAPI, mailer, email, SMTP, send email, templates] | |
5 | +--- | |
6 | + | |
7 | +`Mailer` sends emails over SMTP. It supports synchronous and asynchronous sending, plus Jinja2 templates. | |
8 | + | |
9 | +## Setup | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Mailer | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +mailer = Mailer( | |
17 | + host="smtp.example.com", | |
18 | + port=587, | |
19 | + username="[email protected]", | |
20 | + password="secret", | |
21 | + use_tls=True, | |
22 | + default_from="[email protected]", | |
23 | + default_from_name="My App", | |
24 | +) | |
25 | +``` | |
26 | + | |
27 | +## Send Email | |
28 | + | |
29 | +```python | |
30 | +@app.post("/send") | |
31 | +def send_email(): | |
32 | + mailer.send( | |
33 | + to="[email protected]", | |
34 | + subject="Hello", | |
35 | + body="This is the plain text body", | |
36 | + html="<h1>Hello</h1><p>This is the HTML body</p>", | |
37 | + ) | |
38 | + return {"status": "sent"} | |
39 | +``` | |
40 | + | |
41 | +## Async Send | |
42 | + | |
43 | +```python | |
44 | +@app.post("/send-async") | |
45 | +async def send_email_async(background_tasks: BackgroundTasks): | |
46 | + background_tasks.add_task( | |
47 | + mailer.send_async, | |
48 | + to="[email protected]", | |
49 | + subject="Hello", | |
50 | + body="Sent asynchronously", | |
51 | + ) | |
52 | + return {"status": "queued"} | |
53 | +``` | |
54 | + | |
55 | +## Send with Template | |
56 | + | |
57 | +```python | |
58 | +mailer = Mailer( | |
59 | + host="smtp.example.com", | |
60 | + port=587, | |
61 | + templates="templates/email", | |
62 | +) | |
63 | + | |
64 | +@app.post("/send-template") | |
65 | +def send_welcome(): | |
66 | + mailer.send_template( | |
67 | + template_name="welcome.html", | |
68 | + to="[email protected]", | |
69 | + subject="Welcome!", | |
70 | + context={"name": "Alice"}, | |
71 | + ) | |
72 | + return {"status": "sent"} | |
73 | +``` | |
74 | + | |
75 | +## Parameters | |
76 | + | |
77 | +| Parameter | Type | Default | Description | | |
78 | +|-----------|------|---------|-------------| | |
79 | +| `host` | `str` | — | SMTP server hostname | | |
80 | +| `port` | `int` | `587` | SMTP port | | |
81 | +| `username` | `str` | `None` | Auth username | | |
82 | +| `password` | `str` | `None` | Auth password | | |
83 | +| `use_tls` | `bool` | `True` | Enable TLS | | |
84 | +| `default_from` | `str` | `None` | Default sender email | | |
85 | +| `default_from_name` | `str` | `None` | Default sender name | | |
86 | +| `templates` | `str` | `None` | Directory for Jinja2 email templates | | |
0 | 87 | |
▼docs_site/src/content/docs/api-reference/rate-limiter.md
+69-0
| @@ -0,0 +1,69 @@ | ||
1 | +--- | |
2 | +title: RateLimiter | |
3 | +description: Redis-backed distributed rate limiting with GCRA algorithm in JustAPI. | |
4 | +keywords: [JustAPI, rate limiter, GCRA, Redis, distributed rate limiting] | |
5 | +--- | |
6 | + | |
7 | +## Create Rate Limiter | |
8 | + | |
9 | +```python | |
10 | +from justapi import RateLimiter | |
11 | + | |
12 | +# Async — creates Redis connection | |
13 | +limiter = await RateLimiter.new_redis("redis://localhost:6379") | |
14 | +``` | |
15 | + | |
16 | +## Check Rate Limit | |
17 | + | |
18 | +```python | |
19 | +result = await limiter.check_limit( | |
20 | + key="user:123", # unique key per user/client | |
21 | + capacity=100, # max requests in window | |
22 | + replenish_rate=10, # tokens added per second | |
23 | + tokens=1, # tokens to consume | |
24 | +) | |
25 | + | |
26 | +if result.allowed: | |
27 | + # Process request | |
28 | + return {"status": "ok"} | |
29 | +else: | |
30 | + # Rate limited | |
31 | + return JSONResponse( | |
32 | + status_code=429, | |
33 | + content={"error": "Rate limit exceeded"}, | |
34 | + headers={"Retry-After": str(result.retry_after_ms // 1000)}, | |
35 | + ) | |
36 | +``` | |
37 | + | |
38 | +## Parameters | |
39 | + | |
40 | +| Parameter | Type | Description | | |
41 | +|-----------|------|-------------| | |
42 | +| `key` | `str` | Unique identifier for the rate limit bucket | | |
43 | +| `capacity` | `int` | Maximum burst size | | |
44 | +| `replenish_rate` | `float` | Tokens added per second | | |
45 | +| `tokens` | `int` | Tokens to consume (default: 1) | | |
46 | + | |
47 | +## RateLimitResult | |
48 | + | |
49 | +| Property | Type | Description | | |
50 | +|----------|------|-------------| | |
51 | +| `allowed` | `bool` | Whether the request is allowed | | |
52 | +| `retry_after_ms` | `int` | Milliseconds until next token is available | | |
53 | + | |
54 | +## Built-in Middleware | |
55 | + | |
56 | +JustAPI also provides a built-in rate limiter middleware: | |
57 | + | |
58 | +```python | |
59 | +app.add_middleware( | |
60 | + RateLimiter, | |
61 | + capacity=100, | |
62 | + replenish_rate=10, | |
63 | +) | |
64 | +``` | |
65 | + | |
66 | +## See Also | |
67 | + | |
68 | +- [Resilience Patterns](/advanced/resilience-patterns/) — circuit breakers, retry | |
69 | +- [Security](/tutorials/security/first-steps/) — authentication | |
0 | 70 | |
▼docs_site/src/content/docs/api-reference/session.md
+50-48
| @@ -1,68 +1,70 @@ | ||
1 | 1 | --- |
2 | -title: Session API (Agent System) | |
3 | -description: "API reference for session management in JustAPI, the FastAPI alternative — Rust-backed agent session state management." | |
4 | -keywords: [session, fastapi alternative, justapi, session management, agent system, state management] | |
2 | +title: Session | |
3 | +description: Manage per-user session state in JustAPI — create, read, update, delete sessions. | |
4 | +keywords: [JustAPI, session, state, agent, user session] | |
5 | 5 | --- |
6 | 6 | |
7 | -## `Session` Object | |
8 | - | |
9 | -Sessions provide durable per-client state storage backed by the Rust runtime. Sessions are identified by the `justapi_session` cookie or `?session=` query parameter. | |
7 | +## Enable Sessions | |
10 | 8 | |
11 | 9 | ```python |
12 | -from justapi import Session | |
13 | -``` | |
14 | - | |
15 | -### Properties & Methods | |
16 | - | |
17 | -| Method | Returns | Description | | |
18 | -|---|---|---| | |
19 | -| `session.id` | `str` | Unique session identifier | | |
20 | -| `session.get()` | `dict` | Get all session data | | |
21 | -| `session.update(**kwargs)` | `None` | Update session data with key-value pairs | | |
22 | - | |
23 | -### Usage | |
24 | - | |
25 | -```python | |
26 | -from justapi import JustAPIApp, Session | |
10 | +from justapi import JustAPIApp | |
27 | 11 | |
28 | 12 | app = JustAPIApp() |
29 | -app.enable_sessions() | |
30 | - | |
31 | - | |
32 | -@app.get("/agent/state") | |
33 | -def agent_state(request, session: Session): | |
34 | - prior = session.get().get("visits", 0) | |
35 | - session.update(visits=prior + 1) | |
36 | - return { | |
37 | - "session_id": session.id, | |
38 | - "visits": prior + 1, | |
39 | - "data": session.get(), | |
40 | - } | |
13 | +app.enable_sessions(cookie_name="session_id") | |
41 | 14 | ``` |
42 | 15 | |
43 | -## Enabling Sessions | |
16 | +## Session Methods | |
17 | + | |
18 | +| Method | Description | | |
19 | +|--------|-------------| | |
20 | +| `app.create_session(data, id=None)` | Create a new session | | |
21 | +| `app.get_session(id)` | Get session data by ID | | |
22 | +| `app.set_session(id, data)` | Replace session data | | |
23 | +| `app.update_session(id, **fields)` | Shallow merge fields | | |
24 | +| `app.delete_session(id)` | Delete a session | | |
25 | + | |
26 | +## Example | |
44 | 27 | |
45 | 28 | ```python |
46 | -app.enable_sessions() | |
29 | +@app.post("/sessions") | |
30 | +def create_session(): | |
31 | + session_id = app.create_session({"user_id": 1, "role": "admin"}) | |
32 | + return {"session_id": session_id} | |
33 | + | |
34 | +@app.get("/sessions/{session_id}") | |
35 | +def get_session(session_id: str): | |
36 | + data = app.get_session(session_id) | |
37 | + if data is None: | |
38 | + raise HTTPException(status_code=404, detail="Session not found") | |
39 | + return data | |
40 | + | |
41 | +@app.patch("/sessions/{session_id}") | |
42 | +def update_session(session_id: str, fields: dict): | |
43 | + app.update_session(session_id, **fields) | |
44 | + return {"status": "updated"} | |
45 | + | |
46 | +@app.delete("/sessions/{session_id}") | |
47 | +def delete_session(session_id: str): | |
48 | + app.delete_session(session_id) | |
49 | + return {"status": "deleted"} | |
47 | 50 | ``` |
48 | 51 | |
49 | -This activates the Rust-backed session store. Sessions are: | |
50 | -- Created on first use (new session ID generated) | |
51 | -- Persisted across requests via cookie or query param | |
52 | -- Available in any handler by declaring `session: Session` | |
52 | +## Agent Sessions | |
53 | 53 | |
54 | -## System Routes | |
54 | +Sessions are the foundation for the agent system. The `Session` class wraps session state with a typed interface: | |
55 | 55 | |
56 | 56 | ```python |
57 | -app.enable_system_routes() | |
58 | -``` | |
57 | +from justapi import Session | |
59 | 58 | |
60 | -Enables: | |
61 | -- `GET /_system/tools` — List registered MCP tools | |
62 | -- `POST /_system/tools/call` — Call an MCP tool | |
59 | +@app.get("/agent") | |
60 | +def agent_endpoint(request): | |
61 | + session = Session(request) | |
62 | + session.set("context", {"topic": "weather"}) | |
63 | + context = session.get("context") | |
64 | + return {"context": context} | |
65 | +``` | |
63 | 66 | |
64 | 67 | ## See Also |
65 | 68 | |
66 | -- [Agent System Guide](/advanced/agent-system/) — Deep dive into agent-native features | |
67 | -- [Dependency Injection API](/api-reference/dependency-injection/) — Session in DI | |
68 | -- [JustAPIApp](/api-reference/justapiapp/) — enable_sessions() and enable_system_routes() | |
69 | +- [Agent System](/advanced/agent-system/) — MCP tools and agent sessions | |
70 | +- [Tool Registration](/advanced/agent-system/) — registering tools | |
69 | 71 | |
▼docs_site/src/content/docs/api-reference/system-routes.md
+75-0
| @@ -0,0 +1,75 @@ | ||
1 | +--- | |
2 | +title: System Routes | |
3 | +description: Built-in introspection routes in JustAPI — help, tools, OpenAPI, health checks. | |
4 | +keywords: [JustAPI, system routes, health check, help, introspection, tools] | |
5 | +--- | |
6 | + | |
7 | +## Built-in Routes | |
8 | + | |
9 | +JustAPI registers these routes automatically: | |
10 | + | |
11 | +| Route | Method | Auth | Description | | |
12 | +|-------|--------|------|-------------| | |
13 | +| `/health` | GET | No | Health check (always returns 200) | | |
14 | +| `/live` | GET | No | Liveness probe | | |
15 | +| `/ready` | GET | No | Readiness probe (checks registered health checks) | | |
16 | +| `/metrics` | GET | Yes | Prometheus metrics | | |
17 | +| `/openapi.json` | GET | Yes | OpenAPI 3.1 schema | | |
18 | +| `/docs` | GET | Yes | Swagger UI | | |
19 | +| `/redoc` | GET | Yes | ReDoc | | |
20 | +| `/scalar` | GET | Yes | Scalar API Reference | | |
21 | + | |
22 | +## Enable System Routes | |
23 | + | |
24 | +```python | |
25 | +app = JustAPIApp(admin_token="my-secret-token") | |
26 | +app.enable_system_routes() | |
27 | +``` | |
28 | + | |
29 | +This mounts additional introspection routes: | |
30 | + | |
31 | +| Route | Description | | |
32 | +|-------|-------------| | |
33 | +| `/_system/help` | Rich route descriptors (AI-friendly) | | |
34 | +| `/_system/help/{name}` | Detailed help for one route | | |
35 | +| `/_system/tools` | List registered MCP tools | | |
36 | +| `/_system/tools/call` | Invoke a tool by name | | |
37 | + | |
38 | +## Register Health Checks | |
39 | + | |
40 | +```python | |
41 | +def check_db(): | |
42 | + db = app.db | |
43 | + if db.health(): | |
44 | + return True | |
45 | + raise Exception("Database unreachable") | |
46 | + | |
47 | +def check_redis(): | |
48 | + # Check Redis connection | |
49 | + return True | |
50 | + | |
51 | +app.register_health_check("database", check_db) | |
52 | +app.register_health_check("redis", check_redis) | |
53 | +``` | |
54 | + | |
55 | +`GET /ready` returns 200 only if all health checks pass. | |
56 | + | |
57 | +## Admin Token Protection | |
58 | + | |
59 | +Routes like `/metrics`, `/openapi.json`, `/docs` are protected by `admin_token`: | |
60 | + | |
61 | +```python | |
62 | +app = JustAPIApp(admin_token="my-secret-token") | |
63 | +``` | |
64 | + | |
65 | +Include the token in requests: | |
66 | + | |
67 | +```bash | |
68 | +curl -H "Authorization: Bearer my-secret-token" http://localhost:8000/metrics | |
69 | +``` | |
70 | + | |
71 | +## See Also | |
72 | + | |
73 | +- [Health Checks](/observability/health-checks/) — monitoring | |
74 | +- [Metrics & Monitoring](/observability/metrics-monitoring/) — Prometheus | |
75 | +- [Configuration Reference](/reference/configuration/) — all options | |
0 | 76 | |
▼docs_site/src/content/docs/reference/environment-variables.md
+43-0
| @@ -0,0 +1,43 @@ | ||
1 | +--- | |
2 | +title: Environment Variables | |
3 | +description: Environment variables that control JustAPI behavior — profiling, tracing, background tasks, testing. | |
4 | +keywords: [JustAPI, environment variables, configuration, profiling, tracing] | |
5 | +--- | |
6 | + | |
7 | +JustAPI reads these environment variables at startup: | |
8 | + | |
9 | +## Runtime | |
10 | + | |
11 | +| Variable | Default | Description | | |
12 | +|----------|---------|-------------| | |
13 | +| `JUSTAPI_BG_MAX_QUEUE` | `100000` | Maximum number of queued background tasks | | |
14 | +| `JUSTAPI_ENABLE_TRACE` | `false` | Inject OpenTelemetry trace context into Python contextvars | | |
15 | +| `JUSTAPI_PROFILE` | `false` | Enable GIL-path profiler for performance analysis | | |
16 | + | |
17 | +## Testing | |
18 | + | |
19 | +| Variable | Default | Description | | |
20 | +|----------|---------|-------------| | |
21 | +| `SNAPSHOT_UPDATE` | `false` | Update snapshot files when set to `1`, `true`, or `yes` | | |
22 | + | |
23 | +## Examples | |
24 | + | |
25 | +```bash | |
26 | +# Enable tracing in production | |
27 | +JUSTAPI_ENABLE_TRACE=true python main.py | |
28 | + | |
29 | +# Increase background task queue | |
30 | +JUSTAPI_BG_MAX_QUEUE=500000 python main.py | |
31 | + | |
32 | +# Profile GIL contention | |
33 | +JUSTAPI_PROFILE=true python main.py | |
34 | + | |
35 | +# Update test snapshots | |
36 | +SNAPSHOT_UPDATE=1 pytest tests/ | |
37 | +``` | |
38 | + | |
39 | +## See Also | |
40 | + | |
41 | +- [Configuration Reference](/reference/configuration/) — all config options | |
42 | +- [OpenTelemetry](/observability/opentelemetry/) — tracing setup | |
43 | +- [Background Tasks](/tutorials/background-tasks/) — task execution | |
0 | 44 | |
▼docs_site/src/content/docs/tutorials/frontend.md
+63-0
| @@ -0,0 +1,63 @@ | ||
1 | +--- | |
2 | +title: Frontend | |
3 | +description: Serve a static SPA frontend from JustAPI — React, Vue, Next.js, or any static build. | |
4 | +keywords: [JustAPI, frontend, SPA, static files, React, Vue, Next.js] | |
5 | +--- | |
6 | + | |
7 | +## Serve a Frontend | |
8 | + | |
9 | +Mount a static directory as an SPA: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +@app.frontend("/", directory="dist") | |
17 | +def serve_frontend(): | |
18 | + pass | |
19 | +``` | |
20 | + | |
21 | +This serves `dist/` at `/` and handles client-side routing (any path not matching an API route returns `index.html`). | |
22 | + | |
23 | +## With API Routes | |
24 | + | |
25 | +```python | |
26 | +app = JustAPIApp() | |
27 | + | |
28 | +@app.get("/api/hello") | |
29 | +def hello(): | |
30 | + return {"message": "Hello from API"} | |
31 | + | |
32 | +# Must come AFTER API routes | |
33 | +@app.frontend("/", directory="dist") | |
34 | +def serve_frontend(): | |
35 | + pass | |
36 | +``` | |
37 | + | |
38 | +:::note | |
39 | +Place `frontend()` after all API routes so API paths take priority. | |
40 | +::: | |
41 | + | |
42 | +## Parameters | |
43 | + | |
44 | +| Parameter | Type | Default | Description | | |
45 | +|-----------|------|---------|-------------| | |
46 | +| `path` | `str` | — | Mount path | | |
47 | +| `directory` | `str` | — | Static file directory | | |
48 | +| `html` | `str` | `None` | Custom index HTML (overrides file) | | |
49 | +| `check_dir` | `bool` | `True` | Raise error if directory doesn't exist | | |
50 | + | |
51 | +## Custom HTML | |
52 | + | |
53 | +```python | |
54 | +@app.frontend("/", html="<h1>My App</h1>") | |
55 | +def serve(): | |
56 | + pass | |
57 | +``` | |
58 | + | |
59 | +## See Also | |
60 | + | |
61 | +- [Static Files](/tutorials/static-files/) — serving CSS/JS/images | |
62 | +- [Sub Applications — Mounts](/advanced/sub-applications/) — mounting sub-apps | |
63 | +- [Behind a Proxy](/advanced/behind-a-proxy/) — reverse proxy config | |
0 | 64 | |
▼docs_site/src/content/docs/tutorials/stream-json-lines.md
+53-0
| @@ -0,0 +1,53 @@ | ||
1 | +--- | |
2 | +title: Stream JSON Lines | |
3 | +description: Stream validated JSON responses line-by-line in JustAPI with stream_json. | |
4 | +keywords: [JustAPI, stream JSON, SSE, streaming, NDJSON, validated streaming] | |
5 | +--- | |
6 | + | |
7 | +## Basic Streaming | |
8 | + | |
9 | +Use `stream_json` to send validated JSON lines: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Schema | |
13 | +from typing import AsyncGenerator | |
14 | + | |
15 | +class Token(Schema): | |
16 | + text: str | |
17 | + index: int | |
18 | + | |
19 | +app = JustAPIApp() | |
20 | + | |
21 | +@app.stream_json("/stream", schema=Token, mode="ndjson") | |
22 | +async def stream_tokens(): | |
23 | + for i, word in enumerate(["Hello", "world", "from", "JustAPI"]): | |
24 | + yield Token(text=word, index=i) | |
25 | +``` | |
26 | + | |
27 | +Each line is validated against the schema before sending. | |
28 | + | |
29 | +## Mode | |
30 | + | |
31 | +| Mode | Format | Content-Type | | |
32 | +|------|--------|-------------| | |
33 | +| `"ndjson"` | One JSON object per line | `application/x-ndjson` | | |
34 | +| `"sse"` | Server-Sent Events format | `text/event-stream` | | |
35 | + | |
36 | +## With Dependencies | |
37 | + | |
38 | +```python | |
39 | +@app.stream_json( | |
40 | + "/stream", | |
41 | + schema=Token, | |
42 | + dependencies=[Depends(get_current_user)], | |
43 | +) | |
44 | +async def stream_tokens(user: dict): | |
45 | + for i, word in enumerate(["Hello", "world"]): | |
46 | + yield Token(text=word, index=i) | |
47 | +``` | |
48 | + | |
49 | +## See Also | |
50 | + | |
51 | +- [Streaming Output](/advanced/streaming-output/) — advanced streaming | |
52 | +- [Server-Sent Events](/tutorials/websockets-sse/) — SSE basics | |
53 | +- [Validated Streaming](/advanced/streaming-output/) — schema validation | |
0 | 54 | |
▼docs_site/src/content/docs/tutorials/testing-utilities.md
+91-0
| @@ -0,0 +1,91 @@ | ||
1 | +--- | |
2 | +title: Testing Utilities | |
3 | +description: Advanced testing tools — AsyncTestClient, ManagedDb, Snapshot testing, and assertion helpers. | |
4 | +keywords: [JustAPI, testing, AsyncTestClient, ManagedDb, Snapshot, assertions] | |
5 | +--- | |
6 | + | |
7 | +## AsyncTestClient | |
8 | + | |
9 | +For async tests with pytest-asyncio: | |
10 | + | |
11 | +```python | |
12 | +import pytest | |
13 | +from justapi.testing import AsyncTestClient | |
14 | +from myapp import app | |
15 | + | |
16 | +@pytest.fixture | |
17 | +async def client(): | |
18 | + async with AsyncTestClient(app) as c: | |
19 | + yield c | |
20 | + | |
21 | +@pytest.mark.anyio | |
22 | +async def test_hello(client): | |
23 | + response = await client.get("/hello") | |
24 | + assert response["status"] == 200 | |
25 | +``` | |
26 | + | |
27 | +## ManagedDb | |
28 | + | |
29 | +Test database with automatic cleanup: | |
30 | + | |
31 | +```python | |
32 | +from justapi.testing import ManagedDb | |
33 | + | |
34 | +db = ManagedDb("sqlite:///test.db", migrations_dir="migrations/") | |
35 | + | |
36 | +async def setup(): | |
37 | + await db.run_migrations() | |
38 | + | |
39 | +async def teardown(): | |
40 | + await db.run_sql("DELETE FROM users") | |
41 | +``` | |
42 | + | |
43 | +## db_client | |
44 | + | |
45 | +Create a test client with a managed database: | |
46 | + | |
47 | +```python | |
48 | +from justapi.testing import db_client | |
49 | + | |
50 | +async def test_with_db(): | |
51 | + async with db_client(app, "sqlite:///test.db") as client: | |
52 | + response = await client.get("/users") | |
53 | + assert response["status"] == 200 | |
54 | +``` | |
55 | + | |
56 | +## Snapshot Testing | |
57 | + | |
58 | +Assert responses match saved snapshots: | |
59 | + | |
60 | +```python | |
61 | +from justapi.testing import Snapshot | |
62 | + | |
63 | +snapshot = Snapshot() | |
64 | + | |
65 | +def test_api_response(): | |
66 | + response = client.get("/items") | |
67 | + snapshot.assert_response(response, "items_list") | |
68 | + # First run: saves snapshot | |
69 | + # Subsequent runs: compares against saved snapshot | |
70 | +``` | |
71 | + | |
72 | +Update snapshots by setting `SNAPSHOT_UPDATE=1`. | |
73 | + | |
74 | +## Assertion Helpers | |
75 | + | |
76 | +```python | |
77 | +from justapi.testing import assert_ok, assert_status, assert_json, assert_header | |
78 | + | |
79 | +def test_helpers(): | |
80 | + response = client.get("/hello") | |
81 | + | |
82 | + assert_ok(response) # status == 200 | |
83 | + assert_status(response, 201) # status == 201 | |
84 | + assert_json(response, {"message": "Hello"}) # JSON body matches | |
85 | + assert_header(response, "X-Custom", "value") # header exists with value | |
86 | +``` | |
87 | + | |
88 | +## See Also | |
89 | + | |
90 | +- [Testing](/tutorials/testing/) — basic testing with JustAPITestClient | |
91 | +- [Testing a Database](/how-to/testing-database/) — database testing patterns | |
0 | 92 | |