Spaces:
Running
Running
feat: add qr code genartor api
Browse files- app/api/v1/qr_generator.py +99 -0
- app/api/v1/router.py +2 -1
- app/services/qr_generator_service.py +59 -0
app/api/v1/qr_generator.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import threading
|
| 4 |
+
from typing import Any, Dict, List, Optional
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, HTTPException
|
| 7 |
+
from pydantic import BaseModel, Field, field_validator
|
| 8 |
+
|
| 9 |
+
from app.services.qr_generator_service import QRGeneratorService, QRGeneratorError
|
| 10 |
+
|
| 11 |
+
router = APIRouter()
|
| 12 |
+
|
| 13 |
+
_service_lock = threading.Lock()
|
| 14 |
+
_shared_service: Optional[QRGeneratorService] = None
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _get_service() -> QRGeneratorService:
|
| 18 |
+
global _shared_service
|
| 19 |
+
if _shared_service is None:
|
| 20 |
+
with _service_lock:
|
| 21 |
+
if _shared_service is None:
|
| 22 |
+
_shared_service = QRGeneratorService()
|
| 23 |
+
return _shared_service
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class QRGenerateRequest(BaseModel):
|
| 27 |
+
data: List[str] = Field(
|
| 28 |
+
...,
|
| 29 |
+
min_length=1,
|
| 30 |
+
max_length=500,
|
| 31 |
+
description="List of data strings to generate QR codes for (max 500)",
|
| 32 |
+
)
|
| 33 |
+
size: str = Field(
|
| 34 |
+
"300x300",
|
| 35 |
+
description="QR code image size in WIDTHxHEIGHT format (e.g., 300x300)",
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
@field_validator("data")
|
| 39 |
+
@classmethod
|
| 40 |
+
def validate_data_items(cls, v: List[str]) -> List[str]:
|
| 41 |
+
for i, item in enumerate(v):
|
| 42 |
+
if not item or not item.strip():
|
| 43 |
+
raise ValueError(f"Data item at index {i} must be a non-empty string")
|
| 44 |
+
if len(item) > 2048:
|
| 45 |
+
raise ValueError(
|
| 46 |
+
f"Data item at index {i} exceeds max length of 2048 characters"
|
| 47 |
+
)
|
| 48 |
+
return v
|
| 49 |
+
|
| 50 |
+
@field_validator("size")
|
| 51 |
+
@classmethod
|
| 52 |
+
def validate_size(cls, v: str) -> str:
|
| 53 |
+
parts = v.lower().split("x")
|
| 54 |
+
if len(parts) != 2:
|
| 55 |
+
raise ValueError("Size must be in format WIDTHxHEIGHT (e.g., 300x300)")
|
| 56 |
+
for part in parts:
|
| 57 |
+
if not part.isdigit() or int(part) < 1 or int(part) > 1000:
|
| 58 |
+
raise ValueError("Width and height must be between 1 and 1000")
|
| 59 |
+
return v
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class QRItemResult(BaseModel):
|
| 63 |
+
index: int
|
| 64 |
+
data: str
|
| 65 |
+
qr_image_url: Optional[str] = None
|
| 66 |
+
error: Optional[str] = None
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class QRGenerateResponse(BaseModel):
|
| 70 |
+
success: bool
|
| 71 |
+
total: int
|
| 72 |
+
succeeded: int
|
| 73 |
+
failed: int
|
| 74 |
+
results: List[QRItemResult]
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@router.post(
|
| 78 |
+
"/qr-generator",
|
| 79 |
+
response_model=QRGenerateResponse,
|
| 80 |
+
summary="Generate QR codes for a batch of data strings",
|
| 81 |
+
description="Accepts up to 500 data strings and returns QR code image URLs for each. "
|
| 82 |
+
"The actual QR image is hosted by an external service.",
|
| 83 |
+
)
|
| 84 |
+
def generate_qr_codes(body: QRGenerateRequest):
|
| 85 |
+
service = _get_service()
|
| 86 |
+
try:
|
| 87 |
+
results = service.generate_bulk(body.data, body.size)
|
| 88 |
+
except QRGeneratorError as e:
|
| 89 |
+
raise HTTPException(status_code=400, detail={"success": False, "error": str(e)})
|
| 90 |
+
|
| 91 |
+
succeeded = sum(1 for r in results if r["error"] is None)
|
| 92 |
+
failed = len(results) - succeeded
|
| 93 |
+
return QRGenerateResponse(
|
| 94 |
+
success=failed == 0,
|
| 95 |
+
total=len(results),
|
| 96 |
+
succeeded=succeeded,
|
| 97 |
+
failed=failed,
|
| 98 |
+
results=[QRItemResult(**r) for r in results],
|
| 99 |
+
)
|
app/api/v1/router.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
from fastapi import APIRouter
|
| 4 |
|
| 5 |
-
from app.api.v1 import auth, batch, chat, code_executor, convert, database, embeddings, reconcile, scraper, semantic_router, sql_validator, system, token_counter, token_generator, url_shortener, vector_stores, web_search, webhook_socket
|
| 6 |
from app.api.verify import router as verify_router
|
| 7 |
|
| 8 |
api_v1_router = APIRouter()
|
|
@@ -24,4 +24,5 @@ api_v1_router.include_router(token_generator.router, tags=["Token Generator"])
|
|
| 24 |
api_v1_router.include_router(chat.router, tags=["Chat"])
|
| 25 |
api_v1_router.include_router(vector_stores.router, tags=["Vector Stores"])
|
| 26 |
api_v1_router.include_router(webhook_socket.router, tags=["Webhook / Socket"])
|
|
|
|
| 27 |
api_v1_router.include_router(url_shortener.router, tags=["URL Shortener"])
|
|
|
|
| 2 |
|
| 3 |
from fastapi import APIRouter
|
| 4 |
|
| 5 |
+
from app.api.v1 import auth, batch, chat, code_executor, convert, database, embeddings, qr_generator, reconcile, scraper, semantic_router, sql_validator, system, token_counter, token_generator, url_shortener, vector_stores, web_search, webhook_socket
|
| 6 |
from app.api.verify import router as verify_router
|
| 7 |
|
| 8 |
api_v1_router = APIRouter()
|
|
|
|
| 24 |
api_v1_router.include_router(chat.router, tags=["Chat"])
|
| 25 |
api_v1_router.include_router(vector_stores.router, tags=["Vector Stores"])
|
| 26 |
api_v1_router.include_router(webhook_socket.router, tags=["Webhook / Socket"])
|
| 27 |
+
api_v1_router.include_router(qr_generator.router, tags=["QR Generator"])
|
| 28 |
api_v1_router.include_router(url_shortener.router, tags=["URL Shortener"])
|
app/services/qr_generator_service.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import urllib.parse
|
| 4 |
+
from typing import Any, Dict, List
|
| 5 |
+
|
| 6 |
+
from app.core.logger import get_logger
|
| 7 |
+
|
| 8 |
+
logger = get_logger(__name__)
|
| 9 |
+
|
| 10 |
+
QR_CODE_BASE_URL = "https://api.qrserver.com/v1/create-qr-code/"
|
| 11 |
+
DEFAULT_SIZE = "300x300"
|
| 12 |
+
MAX_DATA_LENGTH = 2048
|
| 13 |
+
MAX_ITEMS = 500
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class QRGeneratorError(Exception):
|
| 17 |
+
pass
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class QRGeneratorService:
|
| 21 |
+
|
| 22 |
+
def generate_bulk(
|
| 23 |
+
self,
|
| 24 |
+
data_list: List[str],
|
| 25 |
+
size: str = DEFAULT_SIZE,
|
| 26 |
+
) -> List[Dict[str, Any]]:
|
| 27 |
+
results: List[Dict[str, Any]] = []
|
| 28 |
+
for idx, data in enumerate(data_list):
|
| 29 |
+
try:
|
| 30 |
+
self._validate_data(data)
|
| 31 |
+
qr_url = self._build_qr_url(data, size)
|
| 32 |
+
results.append({
|
| 33 |
+
"index": idx,
|
| 34 |
+
"data": data,
|
| 35 |
+
"qr_image_url": qr_url,
|
| 36 |
+
"error": None,
|
| 37 |
+
})
|
| 38 |
+
except QRGeneratorError as e:
|
| 39 |
+
results.append({
|
| 40 |
+
"index": idx,
|
| 41 |
+
"data": data,
|
| 42 |
+
"qr_image_url": None,
|
| 43 |
+
"error": str(e),
|
| 44 |
+
})
|
| 45 |
+
return results
|
| 46 |
+
|
| 47 |
+
@staticmethod
|
| 48 |
+
def _validate_data(data: str) -> None:
|
| 49 |
+
if not data or not isinstance(data, str):
|
| 50 |
+
raise QRGeneratorError("Data must be a non-empty string")
|
| 51 |
+
if len(data) > MAX_DATA_LENGTH:
|
| 52 |
+
raise QRGeneratorError(
|
| 53 |
+
f"Data exceeds max length of {MAX_DATA_LENGTH} characters"
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
@staticmethod
|
| 57 |
+
def _build_qr_url(data: str, size: str) -> str:
|
| 58 |
+
encoded = urllib.parse.quote(data, safe="")
|
| 59 |
+
return f"{QR_CODE_BASE_URL}?size={size}&data={encoded}"
|