Backend-сервис для генерации Streamlit-приложений на основе визуально собранной схемы дашборда, работает совместно с frontend сервисом https://github.com/AlexFeed/streamlit_project.
Сервис позволяет:
- Зарегистрировать пользователя и авторизоваться через JWT
- Загрузить CSV-файл на backend
- Получить metadata:
datasetId, имя файла, список колонок - Использовать эти данные в визуальном конструкторе
- Предпросмотреть дашборд через Streamlit preview runtime
- Сгенерировать готовый Streamlit-проект
.zip - Сохранять и управлять проектами дашбордов
- Изолировать проекты и датасеты по пользователям
Frontend (React)
↓
Auth API → JWT accessToken
↓
Authorization: Bearer <token>
↓
Upload CSV → /datasets/upload
↓
datasetId + fields
↓
UI Builder → JSON schema
↓
├──────────────▶ POST /preview
│ ↓
│ Streamlit runtime
│
├──────────────▶ POST /generate
│ ↓
│ ZIP (app.py + data.csv)
│
└──────────────▶ /projects API
↓
project.json (storage)
Editor state (frontend)
↓
Normalized JSON schema
↓
Backend renderers
↓
├── runtime preview
└── final code generation
Один и тот же schema используется для:
- preview
- генерации итогового
app.py
В backend добавлена JWT-авторизация.
После регистрации или логина backend возвращает:
{
"user": {
"id": "uuid",
"email": "test@example.com"
},
"accessToken": "jwt_token"
}Frontend должен сохранять accessToken и передавать его во все защищённые endpoint'ы:
Authorization: Bearer <token>Если токен не передан или невалиден, backend возвращает:
{
"detail": "Authorization header is required"
}или:
{
"detail": "Invalid or expired token"
}Пользователи хранятся локально в JSON-файле:
storage/users/users.json
Пример:
[
{
"id": "uuid",
"email": "test@example.com",
"passwordHash": "$2b$12$..."
}
]Пароли не хранятся в открытом виде. Для хранения используется bcrypt-хеширование.
Данные изолированы по пользователям.
storage/datasets/<userId>/<datasetId>/
data.csv
meta.json
data.csv— загруженный CSV-файлmeta.json— metadata: имя файла, список колонок, размер, дата загрузки
storage/projects/<userId>/<projectId>/
project.json
Пример project.json:
{
"id": "uuid",
"userId": "uuid",
"title": "Sales dashboard",
"description": "",
"datasetMeta": {
"datasetId": "uuid",
"name": "sales.csv",
"fields": ["date", "sales", "region"]
},
"editorState": {
"components": []
},
"schema": {},
"createdAt": "2026-04-27T12:00:00",
"updatedAt": "2026-04-27T12:30:00"
}Backend использует current_user["id"] при работе с проектами и датасетами.
Это означает:
✔ пользователь видит только свои проекты
✔ пользователь видит только свои datasets
✔ пользователь не может получить чужой dataset по datasetId
✔ preview и generate работают только с dataset текущего пользователя
git clone https://github.com/AlexFeed/streamlit_project_backend.git
cd streamlit_project_backendpython -m venv venv
venv\Scripts\activatepython3 -m venv venv
source venv/bin/activatepip install -r requirements.txtuvicorn app.main:app --reloadhttp://127.0.0.1:8000/docs
Все endpoint'ы, кроме /auth/register и /auth/token, требуют авторизацию:
Authorization: Bearer <token>Регистрирует нового пользователя.
{
"email": "test@example.com",
"password": "123456"
}{
"user": {
"id": "4c01eccb-d8ef-4a2f-b2bf-46c9c5f57378",
"email": "test@example.com"
},
"accessToken": "jwt_token"
}Если пользователь уже существует:
{
"detail": "User already exists"
}Если пароль слишком короткий:
{
"detail": "Password must contain at least 6 characters"
}AuthPage
Авторизует пользователя и возвращает JWT-токен.
{
"email": "test@example.com",
"password": "123456"
}{
"user": {
"id": "4c01eccb-d8ef-4a2f-b2bf-46c9c5f57378",
"email": "test@example.com"
},
"accessToken": "jwt_token"
}{
"detail": "Invalid email or password"
}AuthPage
Возвращает текущего пользователя по токену.
GET /auth/me
Authorization: Bearer <token>{
"id": "4c01eccb-d8ef-4a2f-b2bf-46c9c5f57378",
"email": "test@example.com"
}{
"detail": "Authorization header is required"
}или:
{
"detail": "Invalid or expired token"
}ProtectedRoute
Frontend auth check
Загружает CSV-файл на backend.
Используется страницей editor при загрузке датасета.
Authorization: Bearer <token>Content-Type: multipart/form-data
Поле формы:
dataset: File
const formData = new FormData();
formData.append('dataset', file);
const response = await authFetch('/datasets/upload', {
method: 'POST',
body: formData,
});
const datasetMeta = await response.json();curl -X POST "http://127.0.0.1:8000/datasets/upload" \
-H "Authorization: Bearer <token>" \
-F "dataset=@/c/Users/Пользователь/Desktop/test.csv"{
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"userId": "4c01eccb-d8ef-4a2f-b2bf-46c9c5f57378",
"name": "sales.csv",
"storedName": "data.csv",
"fields": ["date", "department", "sales", "revenue"],
"size": 10240,
"createdAt": "2026-04-27T12:00:00"
}Создаётся папка:
storage/datasets/<userId>/<datasetId>/
data.csv
meta.json
EditorPage
useDatasetState.handleFileUpload
Возвращает metadata ранее загруженного датасета.
Backend ищет dataset только внутри папки текущего пользователя.
Authorization: Bearer <token>GET /datasets/9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f{
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"userId": "4c01eccb-d8ef-4a2f-b2bf-46c9c5f57378",
"name": "sales.csv",
"storedName": "data.csv",
"fields": ["date", "department", "sales", "revenue"],
"size": 10240,
"createdAt": "2026-04-27T12:00:00"
}Если dataset не существует или принадлежит другому пользователю:
{
"detail": "Dataset not found"
}Удаляет CSV и meta.json с backend.
Удаление работает только для dataset текущего пользователя.
Authorization: Bearer <token>DELETE /datasets/9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f{
"deleted": true
}{
"detail": "Dataset not found"
}EditorPage
useDatasetState.clearDataset
Projects API нужен для связи главной страницы проектов и editor.
Все проекты изолированы по userId.
Возвращает список проектов текущего пользователя для главной страницы.
Authorization: Bearer <token>GET /projects[
{
"id": "3f49e4c1-6a5a-4db2-b34a-c3b0b0f5d6f1",
"title": "Sales dashboard",
"description": "Анализ продаж по отделам",
"datasetName": "sales.csv",
"createdAt": "2026-04-27T12:00:00",
"updatedAt": "2026-04-27T12:30:00"
},
{
"id": "8c1c1f0d-4c7a-4bb0-92de-8f4f5df1a112",
"title": "Untitled dashboard",
"description": "",
"datasetName": null,
"createdAt": "2026-04-27T13:00:00",
"updatedAt": "2026-04-27T13:00:00"
}
]const response = await authFetch('/projects');
const projects = await response.json();ProjectsPage
Создаёт новый проект для текущего пользователя.
Есть два возможных сценария.
Главная страница создаёт пустой проект и сразу открывает editor:
ProjectsPage → POST /projects → navigate(`/editor/${project.id}`)
Главная страница просто открывает /editor, а проект создаётся только после первого Save в editor:
ProjectsPage → navigate('/editor')
EditorPage → Save project → POST /projects
Для текущей логики предпочтителен второй сценарий.
Authorization: Bearer <token>Минимальный request:
{
"title": "Новый проект",
"description": "Описание проекта"
}Editor может отправить полный payload:
{
"title": "Sales dashboard",
"description": "",
"datasetMeta": {
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "sales", "region"]
},
"editorState": {
"components": []
},
"schema": {}
}{
"id": "3f49e4c1-6a5a-4db2-b34a-c3b0b0f5d6f1",
"userId": "4c01eccb-d8ef-4a2f-b2bf-46c9c5f57378",
"title": "Sales dashboard",
"description": "",
"datasetMeta": {
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "sales", "region"]
},
"editorState": {
"components": []
},
"schema": {},
"createdAt": "2026-04-27T12:00:00",
"updatedAt": "2026-04-27T12:00:00"
}const response = await authFetch('/projects', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Новый проект',
description: '',
}),
});
const project = await response.json();
navigate(`/editor/${project.id}`);Возвращает полный проект для восстановления editor.
Backend ищет проект только внутри папки текущего пользователя.
Authorization: Bearer <token>GET /projects/3f49e4c1-6a5a-4db2-b34a-c3b0b0f5d6f1{
"id": "3f49e4c1-6a5a-4db2-b34a-c3b0b0f5d6f1",
"userId": "4c01eccb-d8ef-4a2f-b2bf-46c9c5f57378",
"title": "Sales dashboard",
"description": "Анализ продаж по отделам",
"datasetMeta": {
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "department", "sales", "revenue"]
},
"editorState": {
"components": [
{
"id": "line_chart-1714219200000-1",
"type": "line_chart",
"config": {
"title": "Sales by date"
},
"bindings": {
"xField": "date",
"yField": "sales"
}
}
]
},
"schema": {
"version": 1,
"dashboard": {
"title": "Untitled dashboard"
},
"dataSource": {
"type": "backend_dataset",
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "department", "sales", "revenue"]
},
"filters": [],
"views": [
{
"id": "line_chart-1714219200000-1",
"type": "line_chart",
"order": 1,
"title": "Sales by date",
"x": "date",
"y": "sales"
}
]
},
"createdAt": "2026-04-27T12:00:00",
"updatedAt": "2026-04-27T12:30:00"
}Если проект не существует или принадлежит другому пользователю:
{
"detail": "Project not found"
}EditorPage при открытии /editor/:projectId
Обновляет проект.
Endpoint работает как частичное обновление.
Если frontend отправляет только title и description, backend не затирает:
datasetMetaeditorStateschema
Authorization: Bearer <token>{
"title": "Новое название проекта",
"description": "Новое описание проекта"
}{
"id": "3f49e4c1-6a5a-4db2-b34a-c3b0b0f5d6f1",
"userId": "4c01eccb-d8ef-4a2f-b2bf-46c9c5f57378",
"title": "Новое название проекта",
"description": "Новое описание проекта",
"datasetMeta": {
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "department", "sales", "revenue"]
},
"editorState": {
"components": []
},
"schema": {},
"createdAt": "2026-04-27T12:00:00",
"updatedAt": "2026-04-27T12:45:00"
}await authFetch(`/projects/${projectId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: newTitle,
description: newDescription,
}),
});{
"title": "Sales dashboard",
"description": "",
"datasetMeta": {
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "department", "sales", "revenue"]
},
"editorState": {
"components": [
{
"id": "metric-1714219200000-1",
"type": "metric",
"config": {
"title": "Total Sales",
"description": "Sum of sales"
},
"bindings": {
"valueField": "sales"
}
}
]
},
"schema": {
"version": 1,
"dashboard": {
"title": "Untitled dashboard"
},
"dataSource": {
"type": "backend_dataset",
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "department", "sales", "revenue"]
},
"filters": [],
"views": [
{
"id": "metric-1714219200000-1",
"type": "metric",
"order": 1,
"title": "Total Sales",
"description": "Sum of sales",
"field": "sales",
"aggregation": "sum"
}
]
}
}Возвращает обновлённый проект.
Удаляет проект из storage/projects/<userId>/<projectId>.
Authorization: Bearer <token>DELETE /projects/3f49e4c1-6a5a-4db2-b34a-c3b0b0f5d6f1{
"deleted": true
}{
"detail": "Project not found"
}await authFetch(`/projects/${projectId}`, {
method: 'DELETE',
});Создаёт preview-сессию.
Backend проверяет dataset внутри папки текущего пользователя.
Authorization: Bearer <token>{
"schema": {
"version": 1,
"dashboard": {
"title": "Untitled dashboard"
},
"dataSource": {
"type": "backend_dataset",
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "sales"]
},
"filters": [],
"views": []
},
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f"
}{
"sessionId": "d38e91f3-87f6-42ea-9d07-9285b7d2e4bc",
"previewUrl": "http://localhost:8501/?session_id=d38e91f3-87f6-42ea-9d07-9285b7d2e4bc"
}EditorPage Preview button
PreviewModal iframe
Внутренний endpoint для Streamlit preview runtime.
Frontend обычно не вызывает этот endpoint напрямую.
Этот endpoint может не требовать Authorization, потому что его вызывает preview_app.py по случайному sessionId.
GET /preview/d38e91f3-87f6-42ea-9d07-9285b7d2e4bc{
"schema": {
"version": 1,
"dashboard": {
"title": "Untitled dashboard"
},
"dataSource": {
"type": "backend_dataset",
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "sales"]
},
"filters": [],
"views": []
},
"datasetPath": "storage/datasets/<userId>/<datasetId>/data.csv"
}Генерирует автономный Streamlit-проект.
Backend проверяет dataset внутри папки текущего пользователя.
Authorization: Bearer <token>{
"schema": {
"version": 1,
"dashboard": {
"title": "Untitled dashboard"
},
"dataSource": {
"type": "backend_dataset",
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f",
"name": "sales.csv",
"fields": ["date", "department", "sales"]
},
"filters": [
{
"id": "selectbox-1",
"type": "selectbox",
"order": 1,
"title": "Department",
"field": "department",
"scope": "global"
}
],
"views": [
{
"id": "line-chart-1",
"type": "line_chart",
"order": 2,
"title": "Sales by Date",
"x": "date",
"y": "sales"
}
]
},
"datasetId": "9b2e9f3a-2b4e-4c91-9e4f-1f2a3c4d5e6f"
}Content-Type: application/zip
Файл:
dashboard_project.zip
Содержимое архива:
app.py
requirements.txt
data/
sales.csv
- Распаковать архив
- Установить зависимости:
pip install -r requirements.txt- Запустить:
streamlit run app.pyВажно:
Сгенерированный app.py автономен и не зависит от FastAPI backend.
AuthPage
↓
POST /auth/login или POST /auth/register
↓
accessToken сохраняется в localStorage
↓
ProtectedRoute открывает доступ к приложению
ProjectsPage → "Новый проект"
↓
navigate('/editor')
↓
EditorPage работает в draft mode
↓
localStorage сохраняет черновик
↓
Save project
↓
POST /projects
↓
localStorage draft очищается
↓
navigate('/editor/:projectId')
ProjectsPage → клик по карточке
↓
navigate('/editor/:projectId')
↓
EditorPage делает GET /projects/{projectId}
↓
восстанавливаются:
- components
- datasetMeta
- schema
ProjectsPage → изменить title/description
↓
PUT /projects/{projectId}
↓
payload содержит только title/description
↓
backend не затирает editorState/schema/datasetMeta
Frontend передаёт на backend нормализованную структуру:
{
"version": 1,
"dashboard": {
"title": "Untitled dashboard"
},
"dataSource": {
"type": "backend_dataset",
"datasetId": "uuid",
"name": "sales.csv",
"fields": ["date", "department", "sales"]
},
"filters": [
{
"id": "selectbox-1",
"type": "selectbox",
"order": 1,
"title": "Department",
"field": "department",
"scope": "global"
}
],
"views": [
{
"id": "line-chart-1",
"type": "line_chart",
"order": 2,
"title": "Sales by Date",
"x": "date",
"y": "sales"
},
{
"id": "metric-1",
"type": "metric",
"order": 3,
"title": "Total Sales",
"description": "Sum of filtered sales",
"field": "sales",
"aggregation": "sum"
}
]
}Текущий pipeline выполнения:
schema
↓
runtime_render → Streamlit preview
code_render → app.py
В будущем планируется выделение отдельного слоя execution plan, чтобы избежать дублирования логики между runtime и code generation.
| Компонент | Описание |
|---|---|
| selectbox | Глобальный фильтр |
| line_chart | Линейный график |
| bar_chart | Столбчатый график |
| metric | Метрика |
auth_service → регистрация, логин, JWT, получение текущего пользователя
dataset_service → работа с CSV и storage/datasets/<userId>/<datasetId>
project_service → хранение проектов в storage/projects/<userId>/<projectId>
preview_service → preview sessions
preview_runtime → запуск Streamlit процесса
generator → сборка app.py
renderers/ → runtime и code rendering
- schema — единый источник правды
- preview и generate используют одну и ту же schema
- backend не зависит от frontend state напрямую
- project.json хранит editorState для восстановления редактора
- итоговый app.py полностью автономен
- localStorage используется только для несохранённого draft
/editor - backend stateless: пользователь определяется по JWT
- userId участвует во всех файловых операциях с проектами и датасетами
- безопасность доступа обеспечивается на backend, а не только на frontend
- JWT используется без refresh tokens
- users хранятся в JSON, а не в БД
- preview-сессии хранятся in-memory
- после перезапуска backend старые previewUrl перестают работать
- поддерживается один dataset на dashboard
- layout/grid пока отсутствует
- ограниченный набор компонентов
- роли пользователей пока отсутствуют
- preview использует отдельный Streamlit процесс (порт 8501)
- FastAPI и Streamlit работают независимо
- при использовании
--reloadвозможны дубли процессов Streamlit - рекомендуется в будущем перейти на lifecycle (
lifespan) - для тестирования API можно использовать Swagger:
http://127.0.0.1:8000/docs - при тестировании через curl нужно передавать токен в формате
Authorization: Bearer <token>
- Валидация schema через Pydantic
- Execution plan
- Aggregations: mean, count, min, max
- Layout/grid
- База данных вместо файлового хранения
- Refresh tokens
- Роли пользователей
- Версионирование проектов
- Облачное хранилище
- Расширение компонентов