This commit is contained in:
parent
3de09ff074
commit
d57548f273
28
core/urlbuild.py
Normal file
28
core/urlbuild.py
Normal file
@ -0,0 +1,28 @@
|
||||
# polisplexity/core/urlbuild.py
|
||||
from django.conf import settings
|
||||
|
||||
def public_base(request=None) -> str:
|
||||
"""
|
||||
1) settings.PUBLIC_BASE_URL (recommended in prod)
|
||||
2) request scheme/host (proxy aware)
|
||||
3) http://localhost:8000 fallback
|
||||
"""
|
||||
base = getattr(settings, "PUBLIC_BASE_URL", "").rstrip("/")
|
||||
if base:
|
||||
return base
|
||||
if request is not None:
|
||||
proto = (request.META.get("HTTP_X_FORWARDED_PROTO")
|
||||
or ("https" if request.is_secure() else "http"))
|
||||
host = request.get_host()
|
||||
return f"{proto.split(',')[0].strip()}://{host}"
|
||||
return "http://localhost:8000"
|
||||
|
||||
def public_url(path_or_abs: str, request=None) -> str:
|
||||
"""Join public base to a path; pass through absolute URLs untouched."""
|
||||
if not path_or_abs:
|
||||
return path_or_abs
|
||||
if "://" in path_or_abs:
|
||||
return path_or_abs
|
||||
if not path_or_abs.startswith("/"):
|
||||
path_or_abs = "/" + path_or_abs
|
||||
return public_base(request) + path_or_abs
|
||||
@ -251,3 +251,9 @@ LOGGING = {
|
||||
},
|
||||
}
|
||||
|
||||
# honor reverse proxy + allow “Host” from proxy
|
||||
USE_X_FORWARDED_HOST = True
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# optional hard override (recommended in prod)
|
||||
PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "").rstrip("/")
|
||||
|
||||
@ -56,6 +56,8 @@ urlpatterns = [
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -11,6 +11,8 @@ from pxy_contracts.contracts import SAMIRunRequest
|
||||
from pxy_sami.estimators.sami_core import run_sami
|
||||
from pxy_dashboard.utils.share import mint_sami_share_url
|
||||
|
||||
# NEW:
|
||||
from core.urlbuild import public_url
|
||||
|
||||
def _err(code: str, message: str, hint: str | None = None, http_status: int = 400):
|
||||
return Response(
|
||||
@ -18,22 +20,15 @@ def _err(code: str, message: str, hint: str | None = None, http_status: int = 40
|
||||
status=http_status,
|
||||
)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@throttle_classes([ScopedRateThrottle])
|
||||
def sami_health(request):
|
||||
sami_health.throttle_scope = "sami_health"
|
||||
try:
|
||||
# If you have deeper checks, put them here. Keep simple/fast.
|
||||
return Response({"ok": True, "service": "sami"})
|
||||
except Exception as e:
|
||||
return _err(
|
||||
"sami_health_error",
|
||||
"SAMI health check failed",
|
||||
str(e),
|
||||
http_status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
return _err("sami_health_error", "SAMI health check failed", str(e),
|
||||
http_status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@api_view(["POST"])
|
||||
@throttle_classes([ScopedRateThrottle])
|
||||
@ -48,7 +43,7 @@ def sami_run(request):
|
||||
resp = run_sami(req)
|
||||
data = resp.model_dump()
|
||||
|
||||
# Inject share URL (signed, expiring)
|
||||
# inject share URL (signed)
|
||||
rid = data.get("run_id")
|
||||
if rid:
|
||||
meta = {
|
||||
@ -59,6 +54,12 @@ def sami_run(request):
|
||||
}
|
||||
data["share_url"] = mint_sami_share_url(rid, meta=meta, request=request)
|
||||
|
||||
# ABSOLUTIZE any path-like URLs (chart, share)
|
||||
for k in ("chart_url", "share_url"):
|
||||
if data.get(k):
|
||||
data[k] = public_url(data[k], request)
|
||||
|
||||
return Response(data)
|
||||
except Exception as e:
|
||||
return _err("sami_error", "SAMI run failed", hint=str(e), http_status=status.HTTP_502_BAD_GATEWAY)
|
||||
return _err("sami_error", "SAMI run failed", hint=str(e),
|
||||
http_status=status.HTTP_502_BAD_GATEWAY)
|
||||
|
||||
@ -25,7 +25,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
from rest_framework import status
|
||||
from rest_framework.throttling import ScopedRateThrottle # 👈 add
|
||||
from rest_framework.throttling import ScopedRateThrottle
|
||||
from pydantic import ValidationError as PydValidationError
|
||||
|
||||
from pxy_contracts.contracts.sites import SiteSearchRequest, SiteSearchResponse
|
||||
@ -33,6 +33,9 @@ from pxy_sites.models import SiteRun
|
||||
from pxy_sites.services.site_scoring import run_site_search
|
||||
from pxy_dashboard.utils.share import mint_sites_share_url
|
||||
|
||||
# NEW: public URL helpers (proxy/HTTPS aware)
|
||||
from core.urlbuild import public_base, public_url
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# -------- uniform error envelope helpers --------
|
||||
@ -59,25 +62,16 @@ def _pyify(o):
|
||||
return o.tolist()
|
||||
return str(o)
|
||||
|
||||
def _build_base_url(request) -> str:
|
||||
forwarded_proto = request.META.get("HTTP_X_FORWARDED_PROTO")
|
||||
scheme = (forwarded_proto.split(",")[0].strip() if forwarded_proto else None) or (
|
||||
"https" if request.is_secure() else "http"
|
||||
)
|
||||
host = request.get_host() or settings.BASE_URL.replace("https://", "").replace("http://", "")
|
||||
return f"{scheme}://{host}"
|
||||
|
||||
# -------- DRF API views --------
|
||||
class SitesHealth(APIView):
|
||||
authentication_classes = []
|
||||
throttle_classes = [ScopedRateThrottle] # 👈 enable throttling
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
throttle_scope = "sites_health"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response({"ok": True, "app": "pxy_sites"})
|
||||
|
||||
class SiteSearchView(APIView):
|
||||
# DRF ScopedRateThrottle is active via project settings; scope name here:
|
||||
throttle_scope = "sites_search"
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@ -86,11 +80,9 @@ class SiteSearchView(APIView):
|
||||
try:
|
||||
req = SiteSearchRequest(**(request.data or {}))
|
||||
except PydValidationError as ve:
|
||||
# DRFValidationError would be handled by your global handler too,
|
||||
# but we return the consistent envelope directly
|
||||
return _env("invalid", "Validation error", hint=str(ve), http=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2) Run scoring (catch provider/upstream failures -> 502 envelope)
|
||||
# 2) Run scoring
|
||||
try:
|
||||
resp: SiteSearchResponse = run_site_search(req)
|
||||
except Exception as e:
|
||||
@ -104,13 +96,21 @@ class SiteSearchView(APIView):
|
||||
|
||||
data = resp.model_dump()
|
||||
|
||||
# 3) Build absolute URLs (proxy-friendly)
|
||||
base = _build_base_url(request)
|
||||
# 3) Public base + absolutize any URLs returned by the scorer
|
||||
base = public_base(request)
|
||||
sid = data.get("search_id")
|
||||
|
||||
if sid:
|
||||
data["share_url"] = mint_sites_share_url(sid, request=request)
|
||||
# ensure top-level map URLs & share are absolute
|
||||
for k in ("map_url", "demand_map_url", "competition_map_url", "share_url"):
|
||||
if data.get(k):
|
||||
data[k] = public_url(data[k], request)
|
||||
|
||||
# inject (signed) share_url if we minted one
|
||||
if sid and not data.get("share_url"):
|
||||
data["share_url"] = mint_sites_share_url(sid, request=request)
|
||||
data["share_url"] = public_url(data["share_url"], request)
|
||||
|
||||
# derived artifact/geojson/download/preview URLs (absolute)
|
||||
def _dl(kind: str) -> str: return f"{base}/api/sites/download/{kind}/{sid}"
|
||||
def _gj(kind: str) -> str: return f"{base}/api/sites/geojson/{kind}/{sid}"
|
||||
def _pv(kind: str) -> str: return f"{base}/api/sites/preview/{kind}/{sid}"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user