Files
svg-backend/backend/app/api/routes/structure.py

361 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from datetime import datetime, timezone
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from fastapi.responses import FileResponse
from lxml import etree
from app.core.config import settings
from app.repositories.pricing import find_effective_price_rule
from app.repositories.scheme_artifacts import get_latest_scheme_artifact
from app.repositories.scheme_groups import list_scheme_version_groups
from app.repositories.scheme_seats import get_scheme_version_seat_by_seat_id, list_scheme_version_seats
from app.repositories.scheme_sectors import list_scheme_version_sectors
from app.repositories.scheme_versions import (
get_current_scheme_version,
update_scheme_version_display_artifact,
)
from app.repositories.schemes import get_scheme_record_by_scheme_id
from app.repositories.uploads import get_upload_record_by_upload_id
from app.schemas.pricing import EffectiveSeatPriceResponse
from app.schemas.scheme_groups import SchemeGroupItem, SchemeGroupListResponse
from app.schemas.scheme_seats import SchemeSeatItem, SchemeSeatListResponse
from app.schemas.scheme_sectors import SchemeSectorItem, SchemeSectorListResponse
from app.security.auth import require_api_key
from app.services.storage import save_display_svg
from app.services.svg_display_processor import ALLOWED_MODES, generate_display_svg
router = APIRouter()
logger = logging.getLogger(__name__)
def _parse_svg_meta_from_bytes(content: bytes) -> dict:
parser = etree.XMLParser(
resolve_entities=False,
remove_blank_text=False,
remove_comments=False,
no_network=True,
recover=False,
huge_tree=True,
)
root = etree.fromstring(content, parser=parser)
return {
"view_box": root.attrib.get("viewBox"),
"width": root.attrib.get("width"),
"height": root.attrib.get("height"),
}
def _resolve_mode(mode: str | None) -> str:
resolved = mode or settings.svg_display_mode
if resolved not in ALLOWED_MODES:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Unsupported display mode: {resolved}",
)
return resolved
async def _load_current_context(scheme_id: str):
scheme = await get_scheme_record_by_scheme_id(scheme_id)
version = await get_current_scheme_version(
scheme_id=scheme.scheme_id,
current_version_number=scheme.current_version_number,
)
upload = await get_upload_record_by_upload_id(scheme.source_upload_id)
return scheme, version, upload
async def _load_default_display_artifact(scheme, version, upload) -> tuple[bytes, Path]:
artifact = await get_latest_scheme_artifact(
scheme_version_id=version.scheme_version_id,
artifact_type="display_svg",
artifact_variant=settings.svg_display_mode,
)
if artifact is not None:
path = Path(artifact.storage_path)
if path.exists() and path.is_file():
return path.read_bytes(), path
if version.display_svg_status == "ready" and version.display_svg_storage_path:
path = Path(version.display_svg_storage_path)
if path.exists() and path.is_file():
return path.read_bytes(), path
sanitized_artifact = await get_latest_scheme_artifact(
scheme_version_id=version.scheme_version_id,
artifact_type="sanitized_svg",
artifact_variant="source",
)
sanitized_path = Path(sanitized_artifact.storage_path if sanitized_artifact else upload.sanitized_storage_path)
if not sanitized_path.exists() or not sanitized_path.is_file():
if version.display_svg_status == "pending":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Current scheme version is not ready for display rendering",
)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Display SVG not found for current scheme version",
)
sanitized_bytes = sanitized_path.read_bytes()
try:
display_bytes, meta = generate_display_svg(sanitized_bytes, settings.svg_display_mode)
except Exception:
logger.exception(
"display_svg.lazy_generate failed scheme_id=%s scheme_version_id=%s mode=%s",
scheme.scheme_id,
version.scheme_version_id,
settings.svg_display_mode,
)
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Current scheme version is not ready for display rendering",
)
display_path_str = save_display_svg(
upload_id=upload.upload_id,
filename=upload.original_filename,
content=display_bytes,
)
display_path = Path(display_path_str)
from app.repositories.scheme_artifacts import create_scheme_artifact
await create_scheme_artifact(
scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id,
artifact_type="display_svg",
artifact_variant=settings.svg_display_mode,
storage_path=display_path_str,
status="ready",
meta_json=meta,
)
await update_scheme_version_display_artifact(
scheme_version_id=version.scheme_version_id,
display_svg_storage_path=display_path_str,
display_svg_status="ready",
display_svg_generated_at=datetime.now(timezone.utc),
)
return display_bytes, display_path
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/current/sectors", response_model=SchemeSectorListResponse)
async def get_scheme_current_sectors(scheme_id: str, role: str = Depends(require_api_key)):
scheme = await get_scheme_record_by_scheme_id(scheme_id)
version = await get_current_scheme_version(scheme_id=scheme.scheme_id, current_version_number=scheme.current_version_number)
rows = await list_scheme_version_sectors(version.scheme_version_id)
items = [
SchemeSectorItem(
sector_record_id=row.sector_record_id,
scheme_id=row.scheme_id,
scheme_version_id=row.scheme_version_id,
element_id=row.element_id,
sector_id=row.sector_id,
name=row.name,
classes_raw=row.classes_raw,
created_at=row.created_at.isoformat(),
)
for row in rows
]
return SchemeSectorListResponse(items=items, total=len(items))
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/current/groups", response_model=SchemeGroupListResponse)
async def get_scheme_current_groups(scheme_id: str, role: str = Depends(require_api_key)):
scheme = await get_scheme_record_by_scheme_id(scheme_id)
version = await get_current_scheme_version(scheme_id=scheme.scheme_id, current_version_number=scheme.current_version_number)
rows = await list_scheme_version_groups(version.scheme_version_id)
items = [
SchemeGroupItem(
group_record_id=row.group_record_id,
scheme_id=row.scheme_id,
scheme_version_id=row.scheme_version_id,
element_id=row.element_id,
group_id=row.group_id,
name=row.name,
classes_raw=row.classes_raw,
created_at=row.created_at.isoformat(),
)
for row in rows
]
return SchemeGroupListResponse(items=items, total=len(items))
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/current/seats", response_model=SchemeSeatListResponse)
async def get_scheme_current_seats(scheme_id: str, role: str = Depends(require_api_key)):
scheme = await get_scheme_record_by_scheme_id(scheme_id)
version = await get_current_scheme_version(scheme_id=scheme.scheme_id, current_version_number=scheme.current_version_number)
rows = await list_scheme_version_seats(version.scheme_version_id)
items = [
SchemeSeatItem(
seat_record_id=row.seat_record_id,
scheme_id=row.scheme_id,
scheme_version_id=row.scheme_version_id,
element_id=row.element_id,
seat_id=row.seat_id,
sector_id=row.sector_id,
group_id=row.group_id,
row_label=row.row_label,
seat_number=row.seat_number,
tag=row.tag,
classes_raw=row.classes_raw,
x=row.x,
y=row.y,
cx=row.cx,
cy=row.cy,
width=row.width,
height=row.height,
created_at=row.created_at.isoformat(),
)
for row in rows
]
return SchemeSeatListResponse(items=items, total=len(items))
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/current/seats/{{seat_id}}/price", response_model=EffectiveSeatPriceResponse)
async def get_effective_seat_price(scheme_id: str, seat_id: str, role: str = Depends(require_api_key)):
scheme = await get_scheme_record_by_scheme_id(scheme_id)
version = await get_current_scheme_version(scheme_id=scheme.scheme_id, current_version_number=scheme.current_version_number)
seat = await get_scheme_version_seat_by_seat_id(scheme_version_id=version.scheme_version_id, seat_id=seat_id)
if not seat.seat_id:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Невозможно рассчитать цену: у места отсутствует seat_id")
matched_rule_level, rule = await find_effective_price_rule(
scheme_id=scheme.scheme_id,
seat_id=seat.seat_id,
group_id=seat.group_id,
sector_id=seat.sector_id,
)
return EffectiveSeatPriceResponse(
scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id,
seat_id=seat.seat_id,
sector_id=seat.sector_id,
group_id=seat.group_id,
matched_rule_level=matched_rule_level,
matched_target_ref=rule["target_ref"],
pricing_category_id=rule["pricing_category_id"],
amount=rule["amount"],
currency=rule["currency"],
)
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/current/svg")
async def get_scheme_current_svg(scheme_id: str, role: str = Depends(require_api_key)):
scheme = await get_scheme_record_by_scheme_id(scheme_id)
upload = await get_upload_record_by_upload_id(scheme.source_upload_id)
svg_path = Path(upload.sanitized_storage_path)
if not svg_path.exists() or not svg_path.is_file():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Current sanitized SVG not found")
filename = f"{scheme.name or scheme.scheme_id}.svg"
return FileResponse(path=svg_path, media_type="image/svg+xml", filename=filename)
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/current/svg/display")
async def get_scheme_current_display_svg(
scheme_id: str,
mode: str | None = Query(default=None),
role: str = Depends(require_api_key),
):
resolved_mode = _resolve_mode(mode)
scheme, version, upload = await _load_current_context(scheme_id)
if resolved_mode == settings.svg_display_mode:
_display_bytes, display_path = await _load_default_display_artifact(scheme, version, upload)
filename = f"{scheme.name or scheme.scheme_id}.{resolved_mode}.svg"
return FileResponse(path=display_path, media_type="image/svg+xml", filename=filename)
artifact = await get_latest_scheme_artifact(
scheme_version_id=version.scheme_version_id,
artifact_type="sanitized_svg",
artifact_variant="source",
)
sanitized_path = Path(artifact.storage_path if artifact else upload.sanitized_storage_path)
if not sanitized_path.exists() or not sanitized_path.is_file():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Display SVG not found for current scheme version",
)
sanitized_bytes = sanitized_path.read_bytes()
try:
display_bytes, _meta = generate_display_svg(sanitized_bytes, resolved_mode)
except Exception:
logger.exception(
"display_svg.on_demand failed scheme_id=%s scheme_version_id=%s mode=%s",
scheme.scheme_id,
version.scheme_version_id,
resolved_mode,
)
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Current scheme version is not ready for display rendering",
)
return Response(content=display_bytes, media_type="image/svg+xml")
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/current/svg/display/meta")
async def get_scheme_current_display_svg_meta(
scheme_id: str,
mode: str | None = Query(default=None),
role: str = Depends(require_api_key),
):
resolved_mode = _resolve_mode(mode)
scheme, version, upload = await _load_current_context(scheme_id)
if resolved_mode == settings.svg_display_mode:
display_bytes, _display_path = await _load_default_display_artifact(scheme, version, upload)
meta = _parse_svg_meta_from_bytes(display_bytes)
generated_at = version.display_svg_generated_at
else:
artifact = await get_latest_scheme_artifact(
scheme_version_id=version.scheme_version_id,
artifact_type="sanitized_svg",
artifact_variant="source",
)
sanitized_path = Path(artifact.storage_path if artifact else upload.sanitized_storage_path)
if not sanitized_path.exists() or not sanitized_path.is_file():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Display SVG not found for current scheme version",
)
sanitized_bytes = sanitized_path.read_bytes()
try:
display_bytes, _meta = generate_display_svg(sanitized_bytes, resolved_mode)
except Exception:
logger.exception(
"display_svg.meta_on_demand failed scheme_id=%s scheme_version_id=%s mode=%s",
scheme.scheme_id,
version.scheme_version_id,
resolved_mode,
)
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Current scheme version is not ready for display rendering",
)
meta = _parse_svg_meta_from_bytes(display_bytes)
generated_at = datetime.now(timezone.utc)
return {
"scheme_id": scheme.scheme_id,
"scheme_version_id": version.scheme_version_id,
"display_svg_available": True,
"view_box": meta["view_box"],
"width": meta["width"],
"height": meta["height"],
"generated_at": generated_at.isoformat() if generated_at else None,
}