feat(maker): AI parsing schede tecniche PDF via OpenRouter
- Nuovo servizio ai_service.py: estrae testo da PDF (pdfplumber) + analisi AI (OpenRouter)
- Endpoint POST /api/recipes/{id}/parse-technical-sheet con validazione file
- UI: bottone "Importa da PDF" nel task editor con modale upload + preview editabile
- Task suggeriti modificabili/rimovibili prima della creazione bulk
- Config: OPENROUTER_API_KEY e OPENROUTER_MODEL in .env
- Dipendenze: pdfplumber + httpx aggiunti a server deps
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,10 @@ MAX_UPLOAD_SIZE_MB=50
|
|||||||
# --- Setup Page ---
|
# --- Setup Page ---
|
||||||
SETUP_PASSWORD= # Password per /api/setup, vuoto = disabilitato
|
SETUP_PASSWORD= # Password per /api/setup, vuoto = disabilitato
|
||||||
|
|
||||||
|
# --- AI (OpenRouter) ---
|
||||||
|
OPENROUTER_API_KEY= # API key per parsing schede tecniche, vuoto = disabilitato
|
||||||
|
OPENROUTER_MODEL=anthropic/claude-sonnet-4 # Modello AI da utilizzare
|
||||||
|
|
||||||
# --- Docker ---
|
# --- Docker ---
|
||||||
DB_ROOT_PASSWORD=root_password_change_me
|
DB_ROOT_PASSWORD=root_password_change_me
|
||||||
NGINX_PORT=80
|
NGINX_PORT=80
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ server = [
|
|||||||
"plotly>=5.0.0",
|
"plotly>=5.0.0",
|
||||||
"kaleido>=0.2.0",
|
"kaleido>=0.2.0",
|
||||||
"weasyprint>=62.0",
|
"weasyprint>=62.0",
|
||||||
|
"pdfplumber>=0.10.0",
|
||||||
|
"httpx>=0.27.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Frontend (Flask tablet UI).
|
# Frontend (Flask tablet UI).
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Recipe router - CRUD, versioning, barcode lookup."""
|
"""Recipe router - CRUD, versioning, barcode lookup, AI parsing."""
|
||||||
from fastapi import APIRouter, Depends, Query
|
import logging
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.backend.database import get_db
|
from src.backend.database import get_db
|
||||||
@@ -164,3 +166,31 @@ async def get_measurement_count(
|
|||||||
"""
|
"""
|
||||||
count = await recipe_service.get_measurement_count(db, recipe_id, version_number)
|
count = await recipe_service.get_measurement_count(db, recipe_id, version_number)
|
||||||
return {"recipe_id": recipe_id, "version_number": version_number, "measurement_count": count}
|
return {"recipe_id": recipe_id, "version_number": version_number, "measurement_count": count}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{recipe_id}/parse-technical-sheet")
|
||||||
|
async def parse_technical_sheet(
|
||||||
|
recipe_id: int,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
_user: User = Depends(require_maker),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Parse a PDF technical sheet using AI and return suggested tasks."""
|
||||||
|
from src.backend.services import ai_service
|
||||||
|
|
||||||
|
if not file.content_type or "pdf" not in file.content_type:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Il file deve essere un PDF")
|
||||||
|
|
||||||
|
pdf_bytes = await file.read()
|
||||||
|
if len(pdf_bytes) > 20 * 1024 * 1024:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="File troppo grande (max 20MB)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tasks = await ai_service.parse_technical_sheet(pdf_bytes)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
logging.getLogger(__name__).error("AI parsing error: %s", e)
|
||||||
|
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="Errore nel servizio AI")
|
||||||
|
|
||||||
|
return {"recipe_id": recipe_id, "suggested_tasks": tasks}
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ class Settings(BaseSettings):
|
|||||||
# Setup page (empty = disabled)
|
# Setup page (empty = disabled)
|
||||||
setup_password: str | None = None
|
setup_password: str | None = None
|
||||||
|
|
||||||
|
# AI / OpenRouter (for technical sheet parsing)
|
||||||
|
openrouter_api_key: str | None = None
|
||||||
|
openrouter_model: str = "anthropic/claude-sonnet-4"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def database_url(self) -> str:
|
def database_url(self) -> str:
|
||||||
"""Async MySQL connection string."""
|
"""Async MySQL connection string."""
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""AI service for parsing technical sheets via OpenRouter."""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import pdfplumber
|
||||||
|
|
||||||
|
from src.backend.config import settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """Sei un assistente specializzato nell'analisi di schede tecniche industriali.
|
||||||
|
Analizza il testo estratto da una scheda tecnica e identifica i blocchi operativi distinti.
|
||||||
|
Per ogni blocco, estrai:
|
||||||
|
- title: titolo breve del blocco (es. "MATERIALI", "TEMPERATURA", "MISURA", "IMBALLO")
|
||||||
|
- directive: istruzione operativa principale (es. "VERIFICARE ATTREZZATURE")
|
||||||
|
- description: dettagli completi del blocco, preservando gli "a capo" originali
|
||||||
|
|
||||||
|
Rispondi SOLO con un array JSON valido, senza markdown o testo aggiuntivo.
|
||||||
|
Esempio:
|
||||||
|
[
|
||||||
|
{"title": "MATERIALI", "directive": "Verificare materiali", "description": "RIGIDO (36): AFT9/UV CRI 7\\nMORBIDO (E38): M70"},
|
||||||
|
{"title": "TEMPERATURA", "directive": "Verificare impostazioni", "description": "RIGIDO: TEMP: 170/170/170/170/170\\nMORBIDO: TEMP: 130-135-140-145"}
|
||||||
|
]"""
|
||||||
|
|
||||||
|
|
||||||
|
def extract_text_from_pdf(pdf_bytes: bytes) -> str:
|
||||||
|
with pdfplumber.open(BytesIO(pdf_bytes)) as pdf:
|
||||||
|
pages = []
|
||||||
|
for page in pdf.pages:
|
||||||
|
text = page.extract_text()
|
||||||
|
if text:
|
||||||
|
pages.append(text)
|
||||||
|
return "\n\n---\n\n".join(pages)
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_technical_sheet(pdf_bytes: bytes) -> list[dict]:
|
||||||
|
text = extract_text_from_pdf(pdf_bytes)
|
||||||
|
if not text.strip():
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not settings.openrouter_api_key:
|
||||||
|
raise ValueError("OPENROUTER_API_KEY non configurata")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=60) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
"https://openrouter.ai/api/v1/chat/completions",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {settings.openrouter_api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
json={
|
||||||
|
"model": settings.openrouter_model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": SYSTEM_PROMPT},
|
||||||
|
{"role": "user", "content": f"Analizza questa scheda tecnica ed estrai i task:\n\n{text}"},
|
||||||
|
],
|
||||||
|
"temperature": 0.1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
data = resp.json()
|
||||||
|
content = data["choices"][0]["message"]["content"].strip()
|
||||||
|
|
||||||
|
# Strip markdown fences if present
|
||||||
|
if content.startswith("```"):
|
||||||
|
content = content.split("\n", 1)[1]
|
||||||
|
if content.endswith("```"):
|
||||||
|
content = content[:-3]
|
||||||
|
content = content.strip()
|
||||||
|
|
||||||
|
return json.loads(content)
|
||||||
@@ -424,3 +424,24 @@ def api_get_measurement_count(recipe_id: int, version_number: int):
|
|||||||
return jsonify(resp), resp.get("status_code", 500)
|
return jsonify(resp), resp.get("status_code", 500)
|
||||||
|
|
||||||
return jsonify(resp), 200
|
return jsonify(resp), 200
|
||||||
|
|
||||||
|
|
||||||
|
@maker_bp.route("/api/recipes/<int:recipe_id>/parse-technical-sheet", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
@role_required("Maker")
|
||||||
|
def api_parse_technical_sheet(recipe_id: int):
|
||||||
|
"""Proxy: Parse PDF technical sheet with AI."""
|
||||||
|
file = request.files.get("file")
|
||||||
|
if not file:
|
||||||
|
return jsonify({"error": True, "detail": "Nessun file caricato"}), 400
|
||||||
|
|
||||||
|
api_key = session.get("api_key", "")
|
||||||
|
base_url = Config.API_SERVER_URL.rstrip("/")
|
||||||
|
resp = http_requests.post(
|
||||||
|
f"{base_url}/api/recipes/{recipe_id}/parse-technical-sheet",
|
||||||
|
headers={"X-API-Key": api_key},
|
||||||
|
files={"file": (file.filename, file.stream, file.content_type)},
|
||||||
|
timeout=90,
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(resp.json()), resp.status_code
|
||||||
|
|||||||
@@ -221,6 +221,7 @@
|
|||||||
<span x-text="tasks.length"></span>
|
<span x-text="tasks.length"></span>
|
||||||
<span x-text="tasks.length === 1 ? '{{ _('task') }}' : '{{ _('task') }}'"></span>
|
<span x-text="tasks.length === 1 ? '{{ _('task') }}' : '{{ _('task') }}'"></span>
|
||||||
</p>
|
</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<button @click="showAddTask = true; $nextTick(() => $refs.newTaskTitle && $refs.newTaskTitle.focus())"
|
<button @click="showAddTask = true; $nextTick(() => $refs.newTaskTitle && $refs.newTaskTitle.focus())"
|
||||||
x-show="!showAddTask"
|
x-show="!showAddTask"
|
||||||
class="btn btn-primary gap-1.5">
|
class="btn btn-primary gap-1.5">
|
||||||
@@ -229,6 +230,15 @@
|
|||||||
</svg>
|
</svg>
|
||||||
{{ _('Aggiungi Task') }}
|
{{ _('Aggiungi Task') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="showAiImport = true"
|
||||||
|
x-show="!showAddTask"
|
||||||
|
class="btn btn-secondary gap-1.5">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
|
</svg>
|
||||||
|
{{ _('Importa da PDF') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ============================================================
|
<!-- ============================================================
|
||||||
@@ -930,6 +940,118 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# ================================================================
|
||||||
|
AI IMPORT MODAL — Upload PDF + preview suggested tasks
|
||||||
|
================================================================ #}
|
||||||
|
<div x-show="showAiImport"
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="transition ease-in duration-150"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
x-cloak
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||||
|
@click.self="showAiImport = false">
|
||||||
|
<div class="bg-[var(--bg-card)] rounded-2xl shadow-2xl max-w-2xl w-full mx-4 max-h-[85vh] flex flex-col border border-[var(--border-color)]">
|
||||||
|
|
||||||
|
{# Header #}
|
||||||
|
<div class="p-5 border-b border-[var(--border-color)] shrink-0">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 rounded-full bg-primary-50 dark:bg-primary-900/30 flex items-center justify-center">
|
||||||
|
<svg class="w-5 h-5 text-primary" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-[var(--text-primary)]">{{ _('Importa da Scheda Tecnica') }}</h3>
|
||||||
|
<p class="text-xs text-[var(--text-secondary)]">{{ _('Carica un PDF e l\'AI estrarrà i task automaticamente') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Body #}
|
||||||
|
<div class="p-5 overflow-y-auto flex-1">
|
||||||
|
{# Upload area (before analysis) #}
|
||||||
|
<div x-show="!aiSuggestions.length && !aiParsing">
|
||||||
|
<label class="flex flex-col items-center justify-center w-full h-40 border-2 border-dashed border-[var(--border-color)] rounded-xl cursor-pointer
|
||||||
|
hover:border-primary hover:bg-primary-50/50 dark:hover:bg-primary-900/10 transition-colors">
|
||||||
|
<svg class="w-10 h-10 text-[var(--text-muted)] mb-2" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium text-[var(--text-secondary)]">{{ _('Clicca per caricare un PDF') }}</span>
|
||||||
|
<span class="text-xs text-[var(--text-muted)] mt-1">{{ _('Max 20MB') }}</span>
|
||||||
|
<input type="file" accept=".pdf,application/pdf" class="hidden" @change="uploadAndParse($event)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Loading #}
|
||||||
|
<div x-show="aiParsing" class="flex flex-col items-center justify-center py-12">
|
||||||
|
<svg class="w-10 h-10 animate-spin text-primary mb-3" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm font-medium text-[var(--text-secondary)]">{{ _('Analisi in corso con AI...') }}</p>
|
||||||
|
<p class="text-xs text-[var(--text-muted)] mt-1">{{ _('Potrebbe richiedere fino a 30 secondi') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Error #}
|
||||||
|
<div x-show="aiError" class="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 mb-4">
|
||||||
|
<p class="text-sm text-red-700 dark:text-red-300" x-text="aiError"></p>
|
||||||
|
<button @click="aiError = ''; aiSuggestions = []" class="text-xs text-red-600 underline mt-1">{{ _('Riprova') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Suggested tasks (editable preview) #}
|
||||||
|
<div x-show="aiSuggestions.length > 0" class="space-y-3">
|
||||||
|
<p class="text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
<span x-text="aiSuggestions.length"></span> {{ _('task suggeriti — modifica o rimuovi prima di confermare') }}
|
||||||
|
</p>
|
||||||
|
<template x-for="(suggestion, idx) in aiSuggestions" :key="idx">
|
||||||
|
<div class="tmf-card">
|
||||||
|
<div class="p-4 space-y-2">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<span class="text-xs font-bold text-primary">Task <span x-text="idx + 1"></span></span>
|
||||||
|
<button @click="aiSuggestions.splice(idx, 1)" class="text-xs text-red-500 hover:text-red-700">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="text" x-model="suggestion.title" class="tmf-input text-sm font-semibold"
|
||||||
|
placeholder="{{ _('Titolo') }}">
|
||||||
|
<input type="text" x-model="suggestion.directive" class="tmf-input text-sm"
|
||||||
|
placeholder="{{ _('Direttiva') }}">
|
||||||
|
<textarea x-model="suggestion.description" class="tmf-input text-sm" rows="3"
|
||||||
|
placeholder="{{ _('Descrizione') }}"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Footer #}
|
||||||
|
<div class="p-4 border-t border-[var(--border-color)] shrink-0 flex items-center justify-between gap-2">
|
||||||
|
<button @click="showAiImport = false; aiSuggestions = []; aiError = ''"
|
||||||
|
class="btn btn-secondary text-sm">
|
||||||
|
{{ _('Annulla') }}
|
||||||
|
</button>
|
||||||
|
<button x-show="aiSuggestions.length > 0"
|
||||||
|
@click="createTasksFromSuggestions()"
|
||||||
|
:disabled="aiCreating"
|
||||||
|
class="btn btn-primary text-sm gap-1.5">
|
||||||
|
<svg x-show="!aiCreating" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
||||||
|
</svg>
|
||||||
|
<svg x-show="aiCreating" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/>
|
||||||
|
</svg>
|
||||||
|
{{ _('Crea') }} <span x-text="aiSuggestions.length"></span> {{ _('task') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -983,6 +1105,13 @@ function taskEditor() {
|
|||||||
// ---- Drag & Drop state ----
|
// ---- Drag & Drop state ----
|
||||||
dragState: { dragging: null, over: null, position: null },
|
dragState: { dragging: null, over: null, position: null },
|
||||||
|
|
||||||
|
// ---- AI Import ----
|
||||||
|
showAiImport: false,
|
||||||
|
aiParsing: false,
|
||||||
|
aiCreating: false,
|
||||||
|
aiSuggestions: [],
|
||||||
|
aiError: '',
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Init
|
// Init
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -1081,6 +1210,80 @@ function taskEditor() {
|
|||||||
this.newTask = { title: '', directive: '', description: '' };
|
this.newTask = { title: '', directive: '', description: '' };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// AI Import
|
||||||
|
// ============================================================
|
||||||
|
async uploadAndParse(event) {
|
||||||
|
var file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
this.aiParsing = true;
|
||||||
|
this.aiError = '';
|
||||||
|
this.aiSuggestions = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
var resp = await fetch('/maker/api/recipes/' + this.recipeId + '/parse-technical-sheet', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRFToken': this.csrfToken() },
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
var data = await resp.json();
|
||||||
|
|
||||||
|
if (!resp.ok || data.error) {
|
||||||
|
this.aiError = data.detail || {{ _("Errore nell'analisi del PDF")|tojson }};
|
||||||
|
} else {
|
||||||
|
this.aiSuggestions = data.suggested_tasks || [];
|
||||||
|
if (!this.aiSuggestions.length) {
|
||||||
|
this.aiError = {{ _("Nessun task identificato nel PDF")|tojson }};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('AI parse error:', err);
|
||||||
|
this.aiError = {{ _("Errore di connessione al server")|tojson }};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.aiParsing = false;
|
||||||
|
event.target.value = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTasksFromSuggestions() {
|
||||||
|
this.aiCreating = true;
|
||||||
|
var created = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < this.aiSuggestions.length; i++) {
|
||||||
|
var s = this.aiSuggestions[i];
|
||||||
|
if (!s.title || !s.title.trim()) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var resp = await fetch('/maker/api/recipes/' + this.recipeId + '/tasks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': this.csrfToken() },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: s.title.trim(),
|
||||||
|
directive: (s.directive || '').trim() || null,
|
||||||
|
description: (s.description || '').trim() || null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
var data = await resp.json();
|
||||||
|
if (!data.error) {
|
||||||
|
if (!data.subtasks) data.subtasks = [];
|
||||||
|
this.tasks.push(data);
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Create task error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.aiCreating = false;
|
||||||
|
this.showAiImport = false;
|
||||||
|
this.aiSuggestions = [];
|
||||||
|
if (created > 0) {
|
||||||
|
this.successMessage = created + ' ' + {{ _("task creati dalla scheda tecnica")|tojson }};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async addTask() {
|
async addTask() {
|
||||||
if (!this.newTask.title.trim()) return;
|
if (!this.newTask.title.trim()) return;
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
|
|||||||
@@ -768,6 +768,42 @@ msgstr "Send signal to management system to start the line timer"
|
|||||||
msgid "Produzione avviata"
|
msgid "Produzione avviata"
|
||||||
msgstr "Production started"
|
msgstr "Production started"
|
||||||
|
|
||||||
|
msgid "Importa da PDF"
|
||||||
|
msgstr "Import from PDF"
|
||||||
|
|
||||||
|
msgid "Importa da Scheda Tecnica"
|
||||||
|
msgstr "Import from Technical Sheet"
|
||||||
|
|
||||||
|
msgid "Carica un PDF e l'AI estrarrà i task automaticamente"
|
||||||
|
msgstr "Upload a PDF and AI will extract tasks automatically"
|
||||||
|
|
||||||
|
msgid "Clicca per caricare un PDF"
|
||||||
|
msgstr "Click to upload a PDF"
|
||||||
|
|
||||||
|
msgid "Analisi in corso con AI..."
|
||||||
|
msgstr "AI analysis in progress..."
|
||||||
|
|
||||||
|
msgid "Potrebbe richiedere fino a 30 secondi"
|
||||||
|
msgstr "May take up to 30 seconds"
|
||||||
|
|
||||||
|
msgid "Riprova"
|
||||||
|
msgstr "Retry"
|
||||||
|
|
||||||
|
msgid "task suggeriti — modifica o rimuovi prima di confermare"
|
||||||
|
msgstr "suggested tasks — edit or remove before confirming"
|
||||||
|
|
||||||
|
msgid "Crea"
|
||||||
|
msgstr "Create"
|
||||||
|
|
||||||
|
msgid "Errore nell'analisi del PDF"
|
||||||
|
msgstr "Error analyzing PDF"
|
||||||
|
|
||||||
|
msgid "Nessun task identificato nel PDF"
|
||||||
|
msgstr "No tasks identified in PDF"
|
||||||
|
|
||||||
|
msgid "task creati dalla scheda tecnica"
|
||||||
|
msgstr "tasks created from technical sheet"
|
||||||
|
|
||||||
msgid "Prossima misurazione tra"
|
msgid "Prossima misurazione tra"
|
||||||
msgstr "Next measurement in"
|
msgstr "Next measurement in"
|
||||||
|
|
||||||
|
|||||||
@@ -798,6 +798,42 @@ msgstr "Invia segnale al gestionale per avviare il timer della linea"
|
|||||||
msgid "Produzione avviata"
|
msgid "Produzione avviata"
|
||||||
msgstr "Produzione avviata"
|
msgstr "Produzione avviata"
|
||||||
|
|
||||||
|
msgid "Importa da PDF"
|
||||||
|
msgstr "Importa da PDF"
|
||||||
|
|
||||||
|
msgid "Importa da Scheda Tecnica"
|
||||||
|
msgstr "Importa da Scheda Tecnica"
|
||||||
|
|
||||||
|
msgid "Carica un PDF e l'AI estrarrà i task automaticamente"
|
||||||
|
msgstr "Carica un PDF e l'AI estrarrà i task automaticamente"
|
||||||
|
|
||||||
|
msgid "Clicca per caricare un PDF"
|
||||||
|
msgstr "Clicca per caricare un PDF"
|
||||||
|
|
||||||
|
msgid "Analisi in corso con AI..."
|
||||||
|
msgstr "Analisi in corso con AI..."
|
||||||
|
|
||||||
|
msgid "Potrebbe richiedere fino a 30 secondi"
|
||||||
|
msgstr "Potrebbe richiedere fino a 30 secondi"
|
||||||
|
|
||||||
|
msgid "Riprova"
|
||||||
|
msgstr "Riprova"
|
||||||
|
|
||||||
|
msgid "task suggeriti — modifica o rimuovi prima di confermare"
|
||||||
|
msgstr "task suggeriti — modifica o rimuovi prima di confermare"
|
||||||
|
|
||||||
|
msgid "Crea"
|
||||||
|
msgstr "Crea"
|
||||||
|
|
||||||
|
msgid "Errore nell'analisi del PDF"
|
||||||
|
msgstr "Errore nell'analisi del PDF"
|
||||||
|
|
||||||
|
msgid "Nessun task identificato nel PDF"
|
||||||
|
msgstr "Nessun task identificato nel PDF"
|
||||||
|
|
||||||
|
msgid "task creati dalla scheda tecnica"
|
||||||
|
msgstr "task creati dalla scheda tecnica"
|
||||||
|
|
||||||
msgid "Prossima misurazione tra"
|
msgid "Prossima misurazione tra"
|
||||||
msgstr "Prossima misurazione tra"
|
msgstr "Prossima misurazione tra"
|
||||||
|
|
||||||
|
|||||||
@@ -554,6 +554,65 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
|
{ url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "48.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cssselect2"
|
name = "cssselect2"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -1063,6 +1122,33 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pdfminer-six"
|
||||||
|
version = "20251230"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "cryptography" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/46/9a/d79d8fa6d47a0338846bb558b39b9963b8eb2dfedec61867c138c1b17eeb/pdfminer_six-20251230.tar.gz", hash = "sha256:e8f68a14c57e00c2d7276d26519ea64be1b48f91db1cdc776faa80528ca06c1e", size = 8511285, upload-time = "2025-12-30T15:49:13.104Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/d7/b288ea32deb752a09aab73c75e1e7572ab2a2b56c3124a5d1eb24c62ceb3/pdfminer_six-20251230-py3-none-any.whl", hash = "sha256:9ff2e3466a7dfc6de6fd779478850b6b7c2d9e9405aa2a5869376a822771f485", size = 6591909, upload-time = "2025-12-30T15:49:10.76Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pdfplumber"
|
||||||
|
version = "0.11.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pdfminer-six" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
{ name = "pypdfium2" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/38/37/9ca3519e92a8434eb93be570b131476cc0a4e840bb39c62ddb7813a39d53/pdfplumber-0.11.9.tar.gz", hash = "sha256:481224b678b2bbdbf376e2c39bf914144eef7c3d301b4a28eebf0f7f6109d6dc", size = 102768, upload-time = "2026-01-05T08:10:29.072Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/c8/cdbc975f5b634e249cfa6597e37c50f3078412474f21c015e508bfbfe3c3/pdfplumber-0.11.9-py3-none-any.whl", hash = "sha256:33ec5580959ba524e9100138746e090879504c42955df1b8a997604dd326c443", size = 60045, upload-time = "2026-01-05T08:10:27.512Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pillow"
|
name = "pillow"
|
||||||
version = "12.2.0"
|
version = "12.2.0"
|
||||||
@@ -1330,6 +1416,35 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pypdfium2"
|
||||||
|
version = "5.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/3d/dc934d3b606c51c3ecc95b6731d84b7dd7ab8e513a50b0e98a4da6c8a719/pypdfium2-5.8.0.tar.gz", hash = "sha256:049397c647e50f83115ee951c49394dab9e9ba52ebdd5a11ab1109390eb3d34e", size = 271934, upload-time = "2026-05-04T17:39:43.794Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/8c/6b75b923cb81368fa3ea7c48a0616b839620a3aeff899885bd930449b89e/pypdfium2-5.8.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:f67b6c74b716d9ac725ad1af49ae786ad813ac20823d45606d59f1fc06caa8af", size = 3374554, upload-time = "2026-05-04T17:39:05.552Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/61/a885c7f36efba89ec98e3d1fe95c83b48c2d6dea321e9194ac6460e7a834/pypdfium2-5.8.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:53e82bf3e6a2da170b1bda83f93b7eec57cb6efe3cacd05cba78823879a85203", size = 2831667, upload-time = "2026-05-04T17:39:08.028Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/1f/04b5627f6dba312d3e707e5b019c9f24d8b03b5aa366866a9e02ec00f8d4/pypdfium2-5.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:085e633dcc89b65ff4035a4787e98ce7ae636836eb39c83dd0db26113d9774bc", size = 3450815, upload-time = "2026-05-04T17:39:09.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/77/8e3a2aba2bc4aef5abe1b1306d05b00588dc0bf7f5c850d1adf6164c786b/pypdfium2-5.8.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:bc84b7c6efede88fcfb9467f81daf416f26b973a54fc1cf4d3410d622fda6d7a", size = 3634395, upload-time = "2026-05-04T17:39:11.225Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/11/6f2b1847d9fa457b3b7251afc2bba2706d104a0c6f01431dfae5d679a839/pypdfium2-5.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63bf09b2e13ba8545c930d243f0650c664a1b51314daa3b5f38df6d1a17b4bc", size = 3617413, upload-time = "2026-05-04T17:39:13.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/fd/99ce639de5ca06d21743c740dd988cd209dda623bc763ae10b8a162022e1/pypdfium2-5.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:937881c1698456749ed203a58db1895baa5eb7178cdb837ef84867790638da28", size = 3347639, upload-time = "2026-05-04T17:39:15.086Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/47/82864cc6e26dd8969d5594c168635acb16458d35cf5fed65d6b2e32abb42/pypdfium2-5.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be9dc2b84a8694ad7e626bab133244e8241014d5ed1930d865a9bdf90df1e24", size = 3746404, upload-time = "2026-05-04T17:39:17.094Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/58/e41e49bba951f61921bac7289e67fe02af5ac57192d0bbfb5f459dc3691d/pypdfium2-5.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f27bd82891ae302dd02d736b14809661f6d1220ee1e96dbed9b23e2811922a3", size = 4177893, upload-time = "2026-05-04T17:39:18.729Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/15/fa7031010d5cf6853dadb4864680a0bfb7782c5bb6a1a401e0c25c4fca87/pypdfium2-5.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26c1089cdbbdc7fe1248f6d17fe3f30214be4f287dd0196b31aaee18a1564240", size = 3665152, upload-time = "2026-05-04T17:39:20.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/6a/5a3520a8b0cfa8d7fdc3f03a07ad9d6146c28ffd519330706f64fd8939a8/pypdfium2-5.8.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c038a9290864aaa4862dd32e591993d82551ca4d152b4e8ce6d43ba37dc04a8", size = 3095365, upload-time = "2026-05-04T17:39:22.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/d3/845bae4de3cfa36865959046156edb5bf9baea400ccdecdd84fdd911b0f5/pypdfium2-5.8.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f104bc1a6d8bfc1ff088aa50db13b9729cfdb3722b44975c3c457e9a7b9c7318", size = 2961801, upload-time = "2026-05-04T17:39:23.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/76/cf54eabee4a172241dfcfe63533bd1e11e2162114a983453a5a40bfec114/pypdfium2-5.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:04ca7c57a553facf8d46c6ea8ba6fa557e698670cfa4a58e0e01fdae2f6be87d", size = 4133067, upload-time = "2026-05-04T17:39:25.619Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/66/dcf871d19187ca04ea184a99801a6e7e556d8347aa49540fee33cda6dfc5/pypdfium2-5.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ad42b9c22477b32dbedcbc8232833f385d92fd0cf92822547b02383cf9a476d7", size = 3749100, upload-time = "2026-05-04T17:39:27.203Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/67/0d456c79660959ca45ad307b4d67161d29f9ed4083ee1e8fe8c6925b7c82/pypdfium2-5.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:388e3119cf5ca0979b7d5f6d40b7fcd5ab49e17ed4e6de6af89ba116061acfda", size = 4339212, upload-time = "2026-05-04T17:39:29.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/89/e5b0e0f7936be341c91c0f45cd70d693878894ed62aed93a6ee32e9c43c4/pypdfium2-5.8.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:aa05bbfa485ce7916217aa78d856c9f9cd86b08b20846c650392a67975ee72e9", size = 4383943, upload-time = "2026-05-04T17:39:31.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/21/4502ed255f082f579cd3537c2971cf1a57778d43703a08bcd1a92253189f/pypdfium2-5.8.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:f0813a16bb39d5ebd173ea5484430bb67a89b4b181db0a636c73b64ad063c3ea", size = 3925680, upload-time = "2026-05-04T17:39:33.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/4f/2e59723e7a07779439bd885c1b4960079c9710603308888d29ac926ae69a/pypdfium2-5.8.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:a3c78f7d20dd821bec6c072efdb21a1370b9efe10fdeeb68c969e67608e25385", size = 4269560, upload-time = "2026-05-04T17:39:34.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/4e/7b6b1bde3788c8b880d4b8131d95d9d339cebafb3ad9102d82e234bb65be/pypdfium2-5.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:86d302e207c138c827b885a72784f7b306d840646ebeae07e8efdbc39321c629", size = 4182434, upload-time = "2026-05-04T17:39:36.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/7b/6ed4782e0d7a5278330598ce8c4b2df7255f4585a0b3d04520fa580d6507/pypdfium2-5.8.0-py3-none-win32.whl", hash = "sha256:3f25fd436920a907291462b41bdc0ab9f8235c3944b4c9c15398da595ffd1fed", size = 3636680, upload-time = "2026-05-04T17:39:38.49Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/55/da7223d4202b2461f4f889b0baf10dddec3db7f88e6fd8c52db4a516eecd/pypdfium2-5.8.0-py3-none-win_amd64.whl", hash = "sha256:55592af0bddd2d62bed18e0053c546c9b72041430c5115e54870f7f6163125b0", size = 3754962, upload-time = "2026-05-04T17:39:40.13Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/7a/f3dcefe6ee7389aad3ca1488c177e8fbf978206de21c7a99ccf487ea38ab/pypdfium2-5.8.0-py3-none-win_arm64.whl", hash = "sha256:3f17ed97ae8a5a1705301ca93af256a5b02f9009dee4e99c5e175831d46ebd7c", size = 3548362, upload-time = "2026-05-04T17:39:42.304Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyphen"
|
name = "pyphen"
|
||||||
version = "0.17.2"
|
version = "0.17.2"
|
||||||
@@ -1643,8 +1758,10 @@ server = [
|
|||||||
{ name = "asyncmy" },
|
{ name = "asyncmy" },
|
||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
{ name = "httpx" },
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "kaleido" },
|
{ name = "kaleido" },
|
||||||
|
{ name = "pdfplumber" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "plotly" },
|
{ name = "plotly" },
|
||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
@@ -1666,8 +1783,10 @@ requires-dist = [
|
|||||||
{ name = "flask-wtf", marker = "extra == 'client'", specifier = ">=1.2.0" },
|
{ name = "flask-wtf", marker = "extra == 'client'", specifier = ">=1.2.0" },
|
||||||
{ name = "gunicorn", marker = "extra == 'client'", specifier = ">=21.0.0" },
|
{ name = "gunicorn", marker = "extra == 'client'", specifier = ">=21.0.0" },
|
||||||
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" },
|
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27.0" },
|
||||||
|
{ name = "httpx", marker = "extra == 'server'", specifier = ">=0.27.0" },
|
||||||
{ name = "jinja2", marker = "extra == 'server'", specifier = ">=3.1.0" },
|
{ name = "jinja2", marker = "extra == 'server'", specifier = ">=3.1.0" },
|
||||||
{ name = "kaleido", marker = "extra == 'server'", specifier = ">=0.2.0" },
|
{ name = "kaleido", marker = "extra == 'server'", specifier = ">=0.2.0" },
|
||||||
|
{ name = "pdfplumber", marker = "extra == 'server'", specifier = ">=0.10.0" },
|
||||||
{ name = "pillow", marker = "extra == 'server'", specifier = ">=10.0.0" },
|
{ name = "pillow", marker = "extra == 'server'", specifier = ">=10.0.0" },
|
||||||
{ name = "plotly", marker = "extra == 'server'", specifier = ">=5.0.0" },
|
{ name = "plotly", marker = "extra == 'server'", specifier = ">=5.0.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.0.0" },
|
{ name = "pydantic", specifier = ">=2.0.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user