feat: add litellm client adapter, JSONL flow detail, and sample output
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"""Explore URA ArcGIS REST API endpoints discovered by auto-reverse."""
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
BASE = "https://maps.ura.gov.sg/ArcGis/rest/services/lsag"
|
||||
|
||||
|
||||
def show(label: str, url: str, params: dict | None = None):
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {label}")
|
||||
print(f" {url}")
|
||||
print(f"{'='*60}")
|
||||
r = requests.get(url, params=params or {}, timeout=15)
|
||||
print(f"Status: {r.status_code}")
|
||||
try:
|
||||
data = r.json()
|
||||
print(json.dumps(data, indent=2)[:3000])
|
||||
except Exception:
|
||||
print(r.text[:1000])
|
||||
|
||||
|
||||
# 1. URA Sale Sites — layer metadata
|
||||
show("URA Sale Sites — Layer Info", f"{BASE}/ura_sale_sites/MapServer/0")
|
||||
|
||||
# 2. URA Sale Sites — query all features
|
||||
show(
|
||||
"URA Sale Sites — All Features",
|
||||
f"{BASE}/ura_sale_sites/MapServer/0/query",
|
||||
{"where": "1=1", "outFields": "*", "f": "json", "resultRecordCount": "5"},
|
||||
)
|
||||
|
||||
# 3. HDB Sale Sites — layer metadata
|
||||
show("HDB Sale Sites — Layer Info", f"{BASE}/hdb_sale_sites2/MapServer/0")
|
||||
|
||||
# 4. HDB Sale Sites — query
|
||||
show(
|
||||
"HDB Sale Sites — All Features",
|
||||
f"{BASE}/hdb_sale_sites2/MapServer/0/query",
|
||||
{"where": "1=1", "outFields": "*", "f": "json", "resultRecordCount": "5"},
|
||||
)
|
||||
|
||||
# 5. JTC Sale Sites — layer metadata
|
||||
show("JTC Sale Sites — Layer Info", f"{BASE}/jtc_sale_sites/MapServer/0")
|
||||
|
||||
# 6. JTC Sale Sites — query
|
||||
show(
|
||||
"JTC Sale Sites — All Features",
|
||||
f"{BASE}/jtc_sale_sites/MapServer/0/query",
|
||||
{"where": "1=1", "outFields": "*", "f": "json", "resultRecordCount": "5"},
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
# example.com API
|
||||
|
||||
## General
|
||||
|
||||
### `GET /`
|
||||
**GET /**
|
||||
|
||||
_Seen 1 time(s)._
|
||||
@@ -0,0 +1,7 @@
|
||||
GET clients2.google.com/time/1/current 200
|
||||
GET example.com/ 200
|
||||
GET www.google.com/async/folae 200
|
||||
POST accounts.google.com/ListAccounts 200
|
||||
GET www.google.com/async/folae 200
|
||||
GET safebrowsingohttpgateway.googleapis.com/v1/ohttp/hpkekeyconfig 200
|
||||
GET example.com/favicon.ico 404
|
||||
@@ -0,0 +1,11 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: example.com API
|
||||
version: 0.0.0
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
summary: GET /
|
||||
responses:
|
||||
2XX:
|
||||
description: GET /
|
||||
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
URA GLS API — Reverse-Engineered from JavaScript source
|
||||
========================================================
|
||||
Endpoints and auth flow discovered by auto-reverse from:
|
||||
https://eservice.ura.gov.sg/maps/?service=GLSRELEASE&site=1
|
||||
|
||||
Auth flow (from OnemapAPI.js + Env.js):
|
||||
1. GET eservice.ura.gov.sg/sharedServicesWeb/onemapService/getOnemapToken (JSONP)
|
||||
→ returns {token: {token: "...", expiry: ...}}
|
||||
2. GET eservice.ura.gov.sg/sharedServicesWeb/onemapService/getOnemapLandLotToken (JSONP)
|
||||
→ returns {token: {token: "...", expiry: ...}}
|
||||
3. OneMap APIs use: Authorization: <token> header
|
||||
|
||||
ArcGIS APIs (maps.ura.gov.sg) — NO auth needed, just geo-blocked to SG IPs.
|
||||
|
||||
Run from a Singapore IP.
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config (from Env.js)
|
||||
# ---------------------------------------------------------------------------
|
||||
ESERVICE = "https://eservice.ura.gov.sg"
|
||||
MAP_HOST = "https://maps.ura.gov.sg"
|
||||
ONEMAP = "https://www.onemap.gov.sg"
|
||||
|
||||
session = requests.Session()
|
||||
session.headers.update({
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Referer": f"{ESERVICE}/maps/",
|
||||
"Origin": ESERVICE,
|
||||
})
|
||||
|
||||
|
||||
def dump(label, resp):
|
||||
print(f"\n{'='*70}")
|
||||
print(f" {label}")
|
||||
print(f" {resp.url}")
|
||||
print(f" Status: {resp.status_code}")
|
||||
print(f"{'='*70}")
|
||||
try:
|
||||
data = resp.json()
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False)[:4000])
|
||||
return data
|
||||
except Exception:
|
||||
print(resp.text[:2000])
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 1: Get OneMap tokens (JSONP endpoints from OnemapAPI.js)
|
||||
# ---------------------------------------------------------------------------
|
||||
print("=" * 70)
|
||||
print(" AUTH: Fetching OneMap tokens")
|
||||
print("=" * 70)
|
||||
|
||||
# These are JSONP endpoints — they wrap the response in a callback function
|
||||
# We need to strip the JSONP wrapper to get the JSON
|
||||
def fetch_jsonp_token(url):
|
||||
"""Fetch a JSONP token endpoint and extract the JSON payload."""
|
||||
resp = session.get(url, timeout=15)
|
||||
text = resp.text.strip()
|
||||
# JSONP format: callbackName({"key":"value"})
|
||||
# Strip the callback wrapper
|
||||
if text.startswith("("):
|
||||
text = text[1:-1]
|
||||
elif "(" in text:
|
||||
start = text.index("(") + 1
|
||||
end = text.rindex(")")
|
||||
text = text[start:end]
|
||||
return json.loads(text)
|
||||
|
||||
try:
|
||||
token_data = fetch_jsonp_token(f"{ESERVICE}/sharedServicesWeb/onemapService/getOnemapToken")
|
||||
onemap_token = token_data.get("token", {}).get("token", "")
|
||||
print(f" OneMap Token: {onemap_token[:50]}...")
|
||||
print(f" Expiry: {token_data.get('token', {}).get('expiry', 'N/A')}")
|
||||
except Exception as e:
|
||||
print(f" Failed to get OneMap token: {e}")
|
||||
onemap_token = ""
|
||||
|
||||
try:
|
||||
landlot_data = fetch_jsonp_token(f"{ESERVICE}/sharedServicesWeb/onemapService/getOnemapLandLotToken")
|
||||
landlot_token = landlot_data.get("token", {}).get("token", "")
|
||||
print(f" LandLot Token: {landlot_token[:50]}...")
|
||||
except Exception as e:
|
||||
print(f" Failed to get LandLot token: {e}")
|
||||
landlot_token = ""
|
||||
|
||||
# Set auth header for OneMap requests
|
||||
if onemap_token:
|
||||
session.headers["Authorization"] = onemap_token
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 2: ArcGIS — URA GLS Sale Sites (the main target)
|
||||
# ---------------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print(" ARCGIS ENDPOINTS (maps.ura.gov.sg)")
|
||||
print("=" * 70)
|
||||
|
||||
# Layer metadata — field names, types, etc.
|
||||
dump(
|
||||
"URA Sale Sites — Layer Metadata",
|
||||
session.get(f"{MAP_HOST}/ArcGis/rest/services/lsag/ura_sale_sites/MapServer/0", params={"f": "json"}, timeout=15),
|
||||
)
|
||||
|
||||
# Query — get first 5 features with all fields
|
||||
dump(
|
||||
"URA Sale Sites — Sample Data",
|
||||
session.get(
|
||||
f"{MAP_HOST}/ArcGis/rest/services/lsag/ura_sale_sites/MapServer/0/query",
|
||||
params={"where": "1=1", "outFields": "*", "f": "json", "resultRecordCount": "5"},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 3: ArcGIS — HDB Sale Sites
|
||||
# ---------------------------------------------------------------------------
|
||||
dump(
|
||||
"HDB Sale Sites — Sample Data",
|
||||
session.get(
|
||||
f"{MAP_HOST}/ArcGis/rest/services/lsag/hdb_sale_sites2/MapServer/0/query",
|
||||
params={"where": "1=1", "outFields": "*", "f": "json", "resultRecordCount": "3"},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 4: ArcGIS — JTC Sale Sites
|
||||
# ---------------------------------------------------------------------------
|
||||
dump(
|
||||
"JTC Sale Sites — Sample Data",
|
||||
session.get(
|
||||
f"{MAP_HOST}/arcgis/rest/services/lsag/jtc_sale_sites/MapServer/0/query",
|
||||
params={"where": "1=1", "outFields": "*", "f": "json", "resultRecordCount": "3"},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 5: OneMap APIs (with auth token)
|
||||
# ---------------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print(" ONEMAP APIs (with Authorization header)")
|
||||
print("=" * 70)
|
||||
|
||||
# Reverse Geocode (Orchard Road)
|
||||
dump(
|
||||
"OneMap Reverse Geocode",
|
||||
session.get(
|
||||
f"{ONEMAP}/api/public/revgeocode",
|
||||
params={"location": "1.3004,103.8460", "addressType": "all", "otherFeatures": "Y"},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
# Land Ownership query
|
||||
if landlot_token:
|
||||
session.headers["Authorization"] = landlot_token
|
||||
dump(
|
||||
"OneMap Land Ownership",
|
||||
session.get(
|
||||
f"{ONEMAP}/api/public/landlotAPI/retrieveLandOwnership",
|
||||
params={"latitude": "1.3004", "longtitude": "103.8460"},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
# Address search
|
||||
if onemap_token:
|
||||
session.headers["Authorization"] = onemap_token
|
||||
dump(
|
||||
"OneMap Address Search",
|
||||
session.get(
|
||||
f"{ONEMAP}/api/common/elastic/search",
|
||||
params={"searchVal": "Orchard Road", "returnGeom": "Y", "getAddrDetails": "Y", "pageNum": "1"},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 6: URA eService APIs (parking, session-based)
|
||||
# ---------------------------------------------------------------------------
|
||||
print("\n" + "=" * 70)
|
||||
print(" URA eSERVICE APIs")
|
||||
print("=" * 70)
|
||||
|
||||
dump(
|
||||
"Parking Place List",
|
||||
session.get(f"{ESERVICE}/ecasService/ppInfo/getParkingPlaceList.do", timeout=15),
|
||||
)
|
||||
|
||||
dump(
|
||||
"Parking Rate Availability",
|
||||
session.get(f"{ESERVICE}/ecasPPService/ppInfo/getPPCurrentRateAvailability.do", timeout=15),
|
||||
)
|
||||
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
URA GLS Map API — Full Sample
|
||||
==============================
|
||||
Shows exactly what the API returns with all headers and data.
|
||||
Run from any IP (ArcGIS endpoints are publicly accessible).
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config (from Env.js)
|
||||
# ---------------------------------------------------------------------------
|
||||
ESERVICE = "https://eservice.ura.gov.sg"
|
||||
MAP_HOST = "https://maps.ura.gov.sg"
|
||||
ONEMAP = "https://www.onemap.gov.sg"
|
||||
|
||||
session = requests.Session()
|
||||
session.headers.update({
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Referer": f"{ESERVICE}/maps/",
|
||||
"Origin": ESERVICE,
|
||||
})
|
||||
|
||||
|
||||
def show(label, resp):
|
||||
"""Print full request/response details."""
|
||||
print(f"\n{'='*70}")
|
||||
print(f" {label}")
|
||||
print(f"{'='*70}")
|
||||
print(f" URL: {resp.url}")
|
||||
print(f" Status: {resp.status_code}")
|
||||
print(f"\n --- Request Headers ---")
|
||||
for k, v in resp.request.headers.items():
|
||||
print(f" {k}: {v}")
|
||||
print(f"\n --- Response Headers ---")
|
||||
for k, v in resp.headers.items():
|
||||
print(f" {k}: {v}")
|
||||
print(f"\n --- Response Body ---")
|
||||
try:
|
||||
data = resp.json()
|
||||
print(json.dumps(data, indent=2, ensure_ascii=False)[:5000])
|
||||
return data
|
||||
except Exception:
|
||||
print(resp.text[:3000])
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Get OneMap auth token (JSONP)
|
||||
# ---------------------------------------------------------------------------
|
||||
print(f"\n{'#'*70}")
|
||||
print(f" STEP 1: Get OneMap Auth Token")
|
||||
print(f"{'#'*70}")
|
||||
|
||||
token_resp = session.get(f"{ESERVICE}/sharedServicesWeb/onemapService/getOnemapToken", timeout=15)
|
||||
print(f"\n URL: {token_resp.url}")
|
||||
print(f" Status: {token_resp.status_code}")
|
||||
print(f" Raw response (JSONP): {token_resp.text[:200]}")
|
||||
|
||||
# Parse JSONP
|
||||
text = token_resp.text.strip()
|
||||
if "(" in text:
|
||||
start = text.index("(") + 1
|
||||
end = text.rindex(")")
|
||||
text = text[start:end]
|
||||
token_data = json.loads(text)
|
||||
onemap_token = token_data["token"]["token"]
|
||||
print(f"\n Parsed token: {onemap_token[:60]}...")
|
||||
print(f" Expiry: {token_data['token']['expiry']}")
|
||||
|
||||
session.headers["Authorization"] = onemap_token
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. URA GLS — Layer Metadata
|
||||
# ---------------------------------------------------------------------------
|
||||
show(
|
||||
"STEP 2: URA GLS Layer Metadata (what fields exist)",
|
||||
session.get(f"{MAP_HOST}/ArcGis/rest/services/lsag/ura_sale_sites/MapServer/0", params={"f": "json"}, timeout=15),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. URA GLS — Query actual parcel data
|
||||
# ---------------------------------------------------------------------------
|
||||
show(
|
||||
"STEP 3: URA GLS Parcels (first 3, all fields)",
|
||||
session.get(
|
||||
f"{MAP_HOST}/ArcGis/rest/services/lsag/ura_sale_sites/MapServer/0/query",
|
||||
params={
|
||||
"where": "1=1",
|
||||
"outFields": "*",
|
||||
"f": "json",
|
||||
"resultRecordCount": "3",
|
||||
},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. URA GLS — Query with filter (upcoming sites only)
|
||||
# ---------------------------------------------------------------------------
|
||||
show(
|
||||
"STEP 4: URA GLS — Filtered (SITE_STATUS = 'Launched')",
|
||||
session.get(
|
||||
f"{MAP_HOST}/ArcGis/rest/services/lsag/ura_sale_sites/MapServer/0/query",
|
||||
params={
|
||||
"where": "SITE_STATUS='Launched for Sale'",
|
||||
"outFields": "NAME_GLS,LOCATION,DEVT_ALLOW,GPR,GFA,SA_SQM,LEASE_YR,DATE_LNCH,DATE_CLOSG,SITE_STATUS",
|
||||
"f": "json",
|
||||
"resultRecordCount": "10",
|
||||
},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. HDB Sale Sites
|
||||
# ---------------------------------------------------------------------------
|
||||
show(
|
||||
"STEP 5: HDB Sale Sites (first 3)",
|
||||
session.get(
|
||||
f"{MAP_HOST}/ArcGis/rest/services/lsag/hdb_sale_sites2/MapServer/0/query",
|
||||
params={
|
||||
"where": "1=1",
|
||||
"outFields": "*",
|
||||
"f": "json",
|
||||
"resultRecordCount": "3",
|
||||
},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. JTC Sale Sites
|
||||
# ---------------------------------------------------------------------------
|
||||
show(
|
||||
"STEP 6: JTC Sale Sites (first 3)",
|
||||
session.get(
|
||||
f"{MAP_HOST}/arcgis/rest/services/lsag/jtc_sale_sites/MapServer/0/query",
|
||||
params={
|
||||
"where": "1=1",
|
||||
"outFields": "*",
|
||||
"f": "json",
|
||||
"resultRecordCount": "3",
|
||||
},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. OneMap Reverse Geocode (with auth header)
|
||||
# ---------------------------------------------------------------------------
|
||||
show(
|
||||
"STEP 7: OneMap Reverse Geocode (Orchard Road)",
|
||||
session.get(
|
||||
f"{ONEMAP}/api/public/revgeocode",
|
||||
params={"location": "1.3004,103.8460", "addressType": "all", "otherFeatures": "Y"},
|
||||
timeout=15,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8. Map Export URL (server-rendered map image)
|
||||
# ---------------------------------------------------------------------------
|
||||
print(f"\n{'#'*70}")
|
||||
print(f" STEP 8: Map Export URLs (open in browser)")
|
||||
print(f"{'#'*70}")
|
||||
|
||||
# Bounding box for Singapore (SVY21 coordinates)
|
||||
# xmin,ymin,xmax,ymax
|
||||
bboxes = {
|
||||
"Central Singapore": "28000,30000,32000,34000",
|
||||
"Full Singapore": "5000,25000,50000,50000",
|
||||
}
|
||||
|
||||
for name, bbox in bboxes.items():
|
||||
url = f"{MAP_HOST}/ArcGis/rest/services/lsag/ura_sale_sites/MapServer/export?f=image&bbox={bbox}&size=800,600&format=png&transparent=false"
|
||||
print(f"\n {name}:")
|
||||
print(f" {url}")
|
||||
|
||||
print(f"\n Open any of these URLs in your browser to see the GLS parcels map!")
|
||||
Reference in New Issue
Block a user