Add API lab

This commit is contained in:
gabdevele
2026-06-02 20:41:41 +02:00 Unverified
parent 3f0a5ecf9b
commit 3997b695f3
5 changed files with 229 additions and 0 deletions
+1
View File
@@ -24,6 +24,7 @@
- Build: `cmake --build build`
- Test: `ctest --test-dir build --output-on-failure`
- OpenAPI: `cmake --build build --target openapi`
- Lab frontend: `streamlit run tools/visione-lab/app.py`
- Local services: `docker compose up -d`
- App: `./build/visione-backend config.json`
+15
View File
@@ -86,6 +86,21 @@ python3 -m json.tool build/openapi/visione.openapi.json >/dev/null
The app serves the generated JSON at `/api/v1/openapi.json` and Swagger UI at
`/api/v1/docs`.
## Visione Lab
`tools/visione-lab` is a small Streamlit frontend used only to test the backend
visually while the real frontend is not ready.
```bash
python3 -m venv .venv-lab
. .venv-lab/bin/activate
pip install -r tools/visione-lab/requirements.txt
streamlit run tools/visione-lab/app.py
```
Keep the backend running on `http://127.0.0.1:5555`. The lab calls the API from
Python, so it is not blocked by browser CORS settings.
## Development Workflow
1. Keep MySQL running with `docker compose up -d`.
+5
View File
@@ -37,3 +37,8 @@ route list, not a manually maintained static Swagger document. The current tool
generates paths, methods, path parameters, bearer auth hints and common JSON or
multipart request bodies. Deeper request/response schemas can be added later by
teaching the generator project-specific conventions or comments.
## Visione Lab
`tools/visione-lab` is a Streamlit dev frontend for visual endpoint testing. It
is intentionally separate from the real product frontend and should stay small:
forms, uploads and response rendering only.
+206
View File
@@ -0,0 +1,206 @@
import json
from typing import Any
import requests
import streamlit as st
st.set_page_config(page_title="Visione Lab", layout="wide")
def api_url(path: str) -> str:
return st.session_state.base_url.rstrip("/") + path
def headers() -> dict[str, str]:
token = st.session_state.get("token", "")
return {"Authorization": f"Bearer {token}"} if token else {}
def show_response(response: requests.Response) -> None:
st.caption(f"{response.request.method} {response.url} -> {response.status_code}")
try:
st.json(response.json())
except ValueError:
st.text(response.text)
def json_input(label: str, value: dict[str, Any]) -> dict[str, Any] | None:
raw = st.text_area(label, json.dumps(value, indent=2), height=180)
try:
return json.loads(raw)
except json.JSONDecodeError as error:
st.error(f"Invalid JSON: {error}")
return None
if "base_url" not in st.session_state:
st.session_state.base_url = "http://127.0.0.1:5555/api/v1"
with st.sidebar:
st.text_input("Backend API", key="base_url")
st.text_input("Bearer token", key="token", type="password")
st.link_button("MailHog", "http://127.0.0.1:8025")
if st.button("Health check"):
show_response(requests.get(api_url("/health"), timeout=5))
st.title("Visione API Lab")
auth_tab, reports_tab, admin_tab, docs_tab = st.tabs(
["Auth", "Reports", "Admin", "Docs"]
)
with auth_tab:
col1, col2 = st.columns(2)
with col1:
st.subheader("Password login")
email = st.text_input("Email", "[email protected]")
password = st.text_input("Password", "ChangeMe123!", type="password")
if st.button("Login"):
response = requests.post(
api_url("/auth/login/password"),
json={"email": email, "password": password},
timeout=10,
)
data = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
token = data.get("token")
if token:
st.session_state.token = token
show_response(response)
st.subheader("OTP")
otp_email = st.text_input("OTP email", "[email protected]")
if st.button("Request OTP"):
show_response(
requests.post(
api_url("/auth/otp/request"),
json={"email": otp_email},
timeout=10,
)
)
otp_code = st.text_input("OTP code")
if st.button("Verify OTP"):
response = requests.post(
api_url("/auth/otp/verify"),
json={"email": otp_email, "code": otp_code},
timeout=10,
)
data = response.json() if response.headers.get("content-type", "").startswith("application/json") else {}
token = data.get("token")
if token:
st.session_state.token = token
show_response(response)
with col2:
st.subheader("Register citizen")
payload = json_input(
"Register payload",
{
"first_name": "Mario",
"last_name": "Rossi",
"email": "[email protected]",
"password": "ChangeMe123!",
"fiscal_code": "RSSMRA80A01H501U",
"phone": "+391234567890",
},
)
if st.button("Register") and payload:
show_response(requests.post(api_url("/auth/register"), json=payload, timeout=10))
st.subheader("Me")
if st.button("GET /me"):
show_response(requests.get(api_url("/me"), headers=headers(), timeout=10))
with reports_tab:
col1, col2 = st.columns(2)
with col1:
st.subheader("Create report")
report_payload = json_input(
"Report payload",
{
"category_id": 1,
"latitude": 41.9028,
"longitude": 12.4964,
"address": "Via Roma 1",
"description": "Lampione non funzionante",
},
)
if st.button("Create report") and report_payload:
show_response(
requests.post(
api_url("/reports"),
json=report_payload,
headers=headers(),
timeout=10,
)
)
st.subheader("List reports")
status = st.text_input("Status filter")
params = {"status": status} if status else {}
if st.button("GET /reports"):
show_response(
requests.get(
api_url("/reports"),
params=params,
headers=headers(),
timeout=10,
)
)
with col2:
report_id = st.number_input("Report id", min_value=1, step=1)
if st.button("Detail"):
show_response(
requests.get(
api_url(f"/reports/{report_id}"),
headers=headers(),
timeout=10,
)
)
st.subheader("Upload media")
files = st.file_uploader(
"Images or videos",
accept_multiple_files=True,
type=["jpg", "jpeg", "png", "webp", "mp4", "webm", "mov"],
)
if st.button("Upload") and files:
multipart = [
("file", (file.name, file.getvalue(), file.type or "application/octet-stream"))
for file in files
]
show_response(
requests.post(
api_url(f"/reports/{report_id}/media"),
headers=headers(),
files=multipart,
timeout=60,
)
)
st.subheader("Workflow")
new_status = st.selectbox(
"Status",
["UNDER_REVIEW", "ASSIGNED", "IN_PROGRESS", "RESOLVED", "REJECTED", "CLOSED"],
)
if st.button("Update status"):
show_response(
requests.patch(
api_url(f"/reports/{report_id}/status"),
json={"status": new_status, "note": "Updated from Visione Lab"},
headers=headers(),
timeout=10,
)
)
with admin_tab:
st.subheader("Reference data")
endpoint = st.selectbox("Endpoint", ["/admin/categories", "/admin/areas", "/admin/teams", "/admin/stats/overview", "/admin/users"])
if st.button("GET admin endpoint"):
show_response(requests.get(api_url(endpoint), headers=headers(), timeout=10))
with docs_tab:
st.link_button("Swagger UI", api_url("/docs"))
if st.button("Fetch OpenAPI"):
show_response(requests.get(api_url("/openapi.json"), timeout=10))
+2
View File
@@ -0,0 +1,2 @@
streamlit>=1.35
requests>=2.31