Spaces:
Running
Running
File size: 5,932 Bytes
fa7eb7f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
"""
URL state encoding/decoding for shareable search URLs.
Core principle: All searches are represented uniformly - basic search is just
advanced search with a single text query at weight +1.
"""
import base64
import json
from typing import Dict, List, Optional, Tuple, Any
try:
from flask import request
except ImportError:
request = None
def encode_search_state(
text_queries: List[str],
text_weights: List[float],
image_queries: List[Tuple[float, float, float]], # (ra, dec, fov)
image_weights: List[float],
rmag_min: float = 13.0,
rmag_max: float = 20.0,
expand_galaxy: Optional[str] = None
) -> str:
"""
Encode search state into a compact base64 string.
Args:
text_queries: List of text query strings
text_weights: List of weights for text queries
image_queries: List of (ra, dec, fov) tuples
image_weights: List of weights for image queries
rmag_min: Minimum r-band magnitude (default 13.0)
rmag_max: Maximum r-band magnitude (default 20.0)
expand_galaxy: Optional object_id to expand in modal after search
Returns:
URL-safe base64 encoded string
"""
state = {}
# Only include non-empty query arrays
if text_queries:
state["tq"] = text_queries
state["tw"] = text_weights
if image_queries:
state["iq"] = image_queries
state["iw"] = image_weights
# Only include magnitude limits if they differ from defaults
if rmag_min != 13.0:
state["rmin"] = rmag_min
if rmag_max != 20.0:
state["rmax"] = rmag_max
# Include expand galaxy if specified
if expand_galaxy:
state["exp"] = expand_galaxy
# Encode to JSON then base64 (URL-safe)
json_str = json.dumps(state, separators=(',', ':')) # Compact JSON
encoded = base64.urlsafe_b64encode(json_str.encode('utf-8')).decode('utf-8')
# Remove padding (= characters) to make URL shorter - we'll add back on decode
return encoded.rstrip('=')
def decode_search_state(encoded: str) -> Dict[str, Any]:
"""
Decode a base64-encoded search state string.
Args:
encoded: URL-safe base64 encoded string
Returns:
Dictionary with full keys:
{
'text_queries': [...],
'text_weights': [...],
'image_queries': [...],
'image_weights': [...],
'rmag_min': float,
'rmag_max': float,
'expand_galaxy': str or None
}
Returns empty/default state if decoding fails.
"""
# Default/empty state
default_state = {
'text_queries': [],
'text_weights': [],
'image_queries': [],
'image_weights': [],
'rmag_min': 13.0,
'rmag_max': 20.0,
'expand_galaxy': None
}
if not encoded:
return default_state
try:
# Add back padding if needed
padding = 4 - (len(encoded) % 4)
if padding != 4:
encoded += '=' * padding
# Decode from base64 then JSON
json_str = base64.urlsafe_b64decode(encoded.encode('utf-8')).decode('utf-8')
compact_state = json.loads(json_str)
# Expand compact keys to full keys
state = {
'text_queries': compact_state.get('tq', []),
'text_weights': compact_state.get('tw', []),
'image_queries': compact_state.get('iq', []),
'image_weights': compact_state.get('iw', []),
'rmag_min': compact_state.get('rmin', 13.0),
'rmag_max': compact_state.get('rmax', 20.0),
'expand_galaxy': compact_state.get('exp', None)
}
return state
except (ValueError, KeyError, json.JSONDecodeError, Exception):
# Silently fail and return empty state for any decode errors
return default_state
def build_share_url(
text_queries: List[str],
text_weights: List[float],
image_queries: List[Tuple[float, float, float]],
image_weights: List[float],
rmag_min: float = 13.0,
rmag_max: float = 20.0,
expand_galaxy: Optional[str] = None
) -> str:
"""
Build a complete shareable URL for the current search state.
Args:
Same as encode_search_state()
Returns:
Complete URL like: https://app.com?s=<encoded_state>
"""
from src.config import URL_STATE_PARAM
# Get base URL from Flask request context
try:
if request and hasattr(request, 'host_url'):
base_url = request.host_url.rstrip('/')
else:
base_url = "http://localhost:8050"
except (RuntimeError, AttributeError):
# If not in request context (e.g., during testing), use a placeholder
base_url = "http://localhost:8050"
# Encode the state
encoded = encode_search_state(
text_queries=text_queries,
text_weights=text_weights,
image_queries=image_queries,
image_weights=image_weights,
rmag_min=rmag_min,
rmag_max=rmag_max,
expand_galaxy=expand_galaxy
)
return f"{base_url}?{URL_STATE_PARAM}={encoded}"
def parse_url_search_param(search_string: str) -> Dict[str, Any]:
"""
Parse the search parameter from a URL query string.
Args:
search_string: The URL search/query string (e.g., "?s=eyJxdWVy...")
Returns:
Decoded state dictionary (same format as decode_search_state)
"""
from src.config import URL_STATE_PARAM
if not search_string:
return decode_search_state("")
# Remove leading '?' if present
search_string = search_string.lstrip('?')
# Parse query parameters
params = {}
for param in search_string.split('&'):
if '=' in param:
key, value = param.split('=', 1)
params[key] = value
# Get the state parameter
encoded = params.get(URL_STATE_PARAM, '')
return decode_search_state(encoded)
|