اضافه شدن قابلیت مقایسه شماتیک
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""Comparison routes using the existing Job, storage, authentication and queue."""
|
||||
import hashlib
|
||||
import json
|
||||
import secrets
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Annotated,Literal
|
||||
from fastapi import File,Form,Header,UploadFile
|
||||
from fastapi.responses import FileResponse,JSONResponse,Response
|
||||
from sqlalchemy import select,func,text
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from .config import settings
|
||||
from .db import Job,sessions
|
||||
from .storage import storage
|
||||
from .uploads import read_schematic
|
||||
from .models.api import ComparisonStatus,ComparisonUploadResponse
|
||||
|
||||
FRONTEND=Path(__file__).resolve().parents[2]/'frontend'
|
||||
|
||||
|
||||
def register_comparison_routes(app, *, authorize, require_api, error):
|
||||
def comparison(identifier,key,token):
|
||||
job=authorize(identifier,key,token,comparison=True)
|
||||
if not job.input_key.endswith('/comparison.json'):error('not_found','Comparison not found',404)
|
||||
return job
|
||||
|
||||
def state(job):
|
||||
result={'success':job.status!='failed','id':job.id,'comparison_id':job.id,'status':job.status,
|
||||
'url':settings().public_url+'/compare/'+job.id,'viewer_url':settings().public_url+'/compare/'+job.id,
|
||||
'status_url':settings().public_url+'/api/v1/comparisons/'+job.id,
|
||||
'expires_at':job.expires_at,'access':job.access}
|
||||
if job.summary:result.update(json.loads(job.summary))
|
||||
if job.error_code:result['error']={'code':job.error_code,'message':job.error_message}
|
||||
return result
|
||||
|
||||
@app.post('/api/compare',status_code=202,response_model=ComparisonUploadResponse,response_model_exclude_unset=True,tags=['Comparisons'])
|
||||
@app.post('/api/v1/comparisons',status_code=202,response_model=ComparisonUploadResponse,response_model_exclude_unset=True,tags=['Comparisons'])
|
||||
async def create(old_file:Annotated[UploadFile,File()],new_file:Annotated[UploadFile,File()],
|
||||
access:Annotated[Literal['private','unlisted'],Form()]='private',
|
||||
expires_in:Annotated[int|None,Form(gt=0)]=None,
|
||||
x_api_key:Annotated[str|None,Header()]=None):
|
||||
require_api(x_api_key)
|
||||
ttl=expires_in or settings().link_ttl
|
||||
if ttl>settings().link_ttl:error('invalid_expiry','expires_in exceeds the configured link lifetime',422)
|
||||
try:
|
||||
old_name,old=await read_schematic(old_file)
|
||||
new_name,new=await read_schematic(new_file)
|
||||
finally:
|
||||
await old_file.close();await new_file.close()
|
||||
identifier=secrets.token_hex(24);token=secrets.token_urlsafe(32);now=time.time()
|
||||
job=Job(id=identifier,filename='Schematic comparison',status='queued',access=access,
|
||||
token_hash=hashlib.sha256(token.encode()).hexdigest(),created_at=now,updated_at=now,
|
||||
expires_at=now+ttl,attempt=0,input_key=identifier+'/comparison.json')
|
||||
files={identifier+'/old.SchDoc':old,identifier+'/new.SchDoc':new,
|
||||
job.input_key:json.dumps({'schema_version':'1.0','old_filename':old_name,'new_filename':new_name}).encode()}
|
||||
def persist():
|
||||
written=[]
|
||||
try:
|
||||
with sessions().begin() as db:
|
||||
if db.bind.dialect.name=='postgresql':db.execute(text('SELECT pg_advisory_xact_lock(84721309)'))
|
||||
pending=db.scalar(select(func.count()).select_from(Job).where(Job.status.in_(['queued','processing']),Job.expires_at>now))
|
||||
if pending>=settings().max_pending:error('queue_full','Rendering queue is full; try again later',503)
|
||||
for key,body in files.items():
|
||||
try: storage().put(key,body)
|
||||
except Exception: error('storage_unavailable','Upload storage unavailable',503)
|
||||
written.append(key)
|
||||
db.add(job);db.flush()
|
||||
except Exception:
|
||||
for key in written:
|
||||
try:storage().delete(key)
|
||||
except Exception:pass
|
||||
raise
|
||||
try:await run_in_threadpool(persist)
|
||||
except OSError:error('storage_unavailable','Upload storage unavailable',503)
|
||||
result=state(job)
|
||||
if access=='private':
|
||||
result['viewer_token']=token
|
||||
result['viewer_url']+='#token='+token
|
||||
result['url']=result['viewer_url']
|
||||
return JSONResponse(result,status_code=202,headers={'Location':result['status_url'],'Retry-After':'1'})
|
||||
|
||||
@app.get('/api/compare/{identifier}',response_model=ComparisonStatus,response_model_exclude_unset=True,tags=['Comparisons'])
|
||||
@app.get('/api/v1/comparisons/{identifier}',response_model=ComparisonStatus,response_model_exclude_unset=True,tags=['Comparisons'])
|
||||
def get(identifier:str,x_api_key:Annotated[str|None,Header()]=None,x_viewer_token:Annotated[str|None,Header()]=None):
|
||||
job=comparison(identifier,x_api_key,x_viewer_token);result=state(job)
|
||||
if job.status=='completed':
|
||||
try:result.update(json.loads(storage().get(job.output_prefix+'/data.json')))
|
||||
except Exception:error('storage_unavailable','Comparison result unavailable',503)
|
||||
return result
|
||||
|
||||
@app.get('/api/v1/comparisons/{identifier}/svg/{side}',response_class=Response,tags=['Comparisons'])
|
||||
def svg(identifier:str,side:Literal['old','new'],x_api_key:Annotated[str|None,Header()]=None,
|
||||
x_viewer_token:Annotated[str|None,Header()]=None):
|
||||
job=comparison(identifier,x_api_key,x_viewer_token)
|
||||
if job.status=='failed':error(job.error_code,job.error_message,422)
|
||||
if job.status!='completed':error('not_ready','Comparison is still in progress',409)
|
||||
try:body=storage().get(job.output_prefix+'/'+side+'.svg')
|
||||
except Exception:error('storage_unavailable','Comparison rendering unavailable',503)
|
||||
return Response(body,media_type='image/svg+xml')
|
||||
|
||||
@app.get('/compare',include_in_schema=False)
|
||||
def upload_page():return FileResponse(FRONTEND/'compare-upload.html')
|
||||
|
||||
@app.get('/compare/{identifier}',include_in_schema=False)
|
||||
def page(identifier:str):
|
||||
import re
|
||||
if not re.fullmatch('[a-f0-9]{48}',identifier):error('not_found','Comparison not found',404)
|
||||
return FileResponse(FRONTEND/'compare.html')
|
||||
مرجع در شماره جدید
مسدود کردن کاربر