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_sami.estimators.sami_core import run_sami
|
||||||
from pxy_dashboard.utils.share import mint_sami_share_url
|
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):
|
def _err(code: str, message: str, hint: str | None = None, http_status: int = 400):
|
||||||
return Response(
|
return Response(
|
||||||
@ -18,22 +20,15 @@ def _err(code: str, message: str, hint: str | None = None, http_status: int = 40
|
|||||||
status=http_status,
|
status=http_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
@throttle_classes([ScopedRateThrottle])
|
@throttle_classes([ScopedRateThrottle])
|
||||||
def sami_health(request):
|
def sami_health(request):
|
||||||
sami_health.throttle_scope = "sami_health"
|
sami_health.throttle_scope = "sami_health"
|
||||||
try:
|
try:
|
||||||
# If you have deeper checks, put them here. Keep simple/fast.
|
|
||||||
return Response({"ok": True, "service": "sami"})
|
return Response({"ok": True, "service": "sami"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _err(
|
return _err("sami_health_error", "SAMI health check failed", str(e),
|
||||||
"sami_health_error",
|
http_status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
"SAMI health check failed",
|
|
||||||
str(e),
|
|
||||||
http_status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
@throttle_classes([ScopedRateThrottle])
|
@throttle_classes([ScopedRateThrottle])
|
||||||
@ -48,7 +43,7 @@ def sami_run(request):
|
|||||||
resp = run_sami(req)
|
resp = run_sami(req)
|
||||||
data = resp.model_dump()
|
data = resp.model_dump()
|
||||||
|
|
||||||
# Inject share URL (signed, expiring)
|
# inject share URL (signed)
|
||||||
rid = data.get("run_id")
|
rid = data.get("run_id")
|
||||||
if rid:
|
if rid:
|
||||||
meta = {
|
meta = {
|
||||||
@ -59,6 +54,12 @@ def sami_run(request):
|
|||||||
}
|
}
|
||||||
data["share_url"] = mint_sami_share_url(rid, meta=meta, request=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)
|
return Response(data)
|
||||||
except Exception as e:
|
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.response import Response
|
||||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||||
from rest_framework import status
|
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 pydantic import ValidationError as PydValidationError
|
||||||
|
|
||||||
from pxy_contracts.contracts.sites import SiteSearchRequest, SiteSearchResponse
|
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_sites.services.site_scoring import run_site_search
|
||||||
from pxy_dashboard.utils.share import mint_sites_share_url
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# -------- uniform error envelope helpers --------
|
# -------- uniform error envelope helpers --------
|
||||||
@ -59,25 +62,16 @@ def _pyify(o):
|
|||||||
return o.tolist()
|
return o.tolist()
|
||||||
return str(o)
|
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 --------
|
# -------- DRF API views --------
|
||||||
class SitesHealth(APIView):
|
class SitesHealth(APIView):
|
||||||
authentication_classes = []
|
authentication_classes = []
|
||||||
throttle_classes = [ScopedRateThrottle] # 👈 enable throttling
|
throttle_classes = [ScopedRateThrottle]
|
||||||
throttle_scope = "sites_health"
|
throttle_scope = "sites_health"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
return Response({"ok": True, "app": "pxy_sites"})
|
return Response({"ok": True, "app": "pxy_sites"})
|
||||||
|
|
||||||
class SiteSearchView(APIView):
|
class SiteSearchView(APIView):
|
||||||
# DRF ScopedRateThrottle is active via project settings; scope name here:
|
|
||||||
throttle_scope = "sites_search"
|
throttle_scope = "sites_search"
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@ -86,11 +80,9 @@ class SiteSearchView(APIView):
|
|||||||
try:
|
try:
|
||||||
req = SiteSearchRequest(**(request.data or {}))
|
req = SiteSearchRequest(**(request.data or {}))
|
||||||
except PydValidationError as ve:
|
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)
|
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:
|
try:
|
||||||
resp: SiteSearchResponse = run_site_search(req)
|
resp: SiteSearchResponse = run_site_search(req)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -104,13 +96,21 @@ class SiteSearchView(APIView):
|
|||||||
|
|
||||||
data = resp.model_dump()
|
data = resp.model_dump()
|
||||||
|
|
||||||
# 3) Build absolute URLs (proxy-friendly)
|
# 3) Public base + absolutize any URLs returned by the scorer
|
||||||
base = _build_base_url(request)
|
base = public_base(request)
|
||||||
sid = data.get("search_id")
|
sid = data.get("search_id")
|
||||||
|
|
||||||
if sid:
|
# ensure top-level map URLs & share are absolute
|
||||||
data["share_url"] = mint_sites_share_url(sid, request=request)
|
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 _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 _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}"
|
def _pv(kind: str) -> str: return f"{base}/api/sites/preview/{kind}/{sid}"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user