refactor(web): update all references to location_profiles
Update api.py (3 endpoints), public/routes.py, analytics.py docstring, pipeline_routes.py DAG, pipeline_query.html placeholder, and test_pipeline.py fixtures to use the new unified model. Subtask 3/5: web app references. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -111,13 +111,12 @@ _DAG: dict[str, list[str]] = {
|
|||||||
"fct_daily_availability": ["fct_availability_slot", "dim_venue_capacity"],
|
"fct_daily_availability": ["fct_availability_slot", "dim_venue_capacity"],
|
||||||
# Serving
|
# Serving
|
||||||
"venue_pricing_benchmarks": ["fct_daily_availability"],
|
"venue_pricing_benchmarks": ["fct_daily_availability"],
|
||||||
"city_market_profile": ["dim_cities", "venue_pricing_benchmarks"],
|
"location_profiles": ["dim_locations", "dim_cities", "venue_pricing_benchmarks"],
|
||||||
"planner_defaults": ["venue_pricing_benchmarks", "city_market_profile"],
|
"planner_defaults": ["venue_pricing_benchmarks", "location_profiles"],
|
||||||
"location_opportunity_profile": ["dim_locations"],
|
|
||||||
"pseo_city_costs_de": [
|
"pseo_city_costs_de": [
|
||||||
"city_market_profile", "planner_defaults", "location_opportunity_profile",
|
"location_profiles", "planner_defaults",
|
||||||
],
|
],
|
||||||
"pseo_city_pricing": ["venue_pricing_benchmarks", "city_market_profile"],
|
"pseo_city_pricing": ["venue_pricing_benchmarks", "location_profiles"],
|
||||||
"pseo_country_overview": ["pseo_city_costs_de"],
|
"pseo_country_overview": ["pseo_city_costs_de"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
placeholder="-- SELECT * FROM serving.city_market_profile -- WHERE country_code = 'DE' -- ORDER BY marktreife_score DESC -- LIMIT 20"
|
placeholder="-- SELECT * FROM serving.location_profiles -- WHERE country_code = 'DE' AND city_slug IS NOT NULL -- ORDER BY market_score DESC -- LIMIT 20"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<div class="query-controls">
|
<div class="query-controls">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Usage:
|
|||||||
|
|
||||||
rows = await fetch_analytics("SELECT * FROM serving.planner_defaults WHERE city_slug = ?", ["berlin"])
|
rows = await fetch_analytics("SELECT * FROM serving.planner_defaults WHERE city_slug = ?", ["berlin"])
|
||||||
|
|
||||||
cols, rows, error, elapsed_ms = await execute_user_query("SELECT city_slug FROM serving.city_market_profile LIMIT 5")
|
cols, rows, error, elapsed_ms = await execute_user_query("SELECT city_slug FROM serving.location_profiles LIMIT 5")
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|||||||
@@ -32,12 +32,14 @@ async def countries():
|
|||||||
rows = await fetch_analytics("""
|
rows = await fetch_analytics("""
|
||||||
SELECT country_code, country_name_en, country_slug,
|
SELECT country_code, country_name_en, country_slug,
|
||||||
COUNT(*) AS city_count,
|
COUNT(*) AS city_count,
|
||||||
SUM(padel_venue_count) AS total_venues,
|
SUM(city_padel_venue_count) AS total_venues,
|
||||||
ROUND(AVG(market_score), 1) AS avg_market_score,
|
ROUND(AVG(market_score), 1) AS avg_market_score,
|
||||||
|
ROUND(AVG(opportunity_score), 1) AS avg_opportunity_score,
|
||||||
AVG(lat) AS lat, AVG(lon) AS lon
|
AVG(lat) AS lat, AVG(lon) AS lon
|
||||||
FROM serving.city_market_profile
|
FROM serving.location_profiles
|
||||||
|
WHERE city_slug IS NOT NULL
|
||||||
GROUP BY country_code, country_name_en, country_slug
|
GROUP BY country_code, country_name_en, country_slug
|
||||||
HAVING SUM(padel_venue_count) > 0
|
HAVING SUM(city_padel_venue_count) > 0
|
||||||
ORDER BY total_venues DESC
|
ORDER BY total_venues DESC
|
||||||
""")
|
""")
|
||||||
return jsonify(rows), 200, _CACHE_HEADERS
|
return jsonify(rows), 200, _CACHE_HEADERS
|
||||||
@@ -51,10 +53,11 @@ async def country_cities(country_slug: str):
|
|||||||
rows = await fetch_analytics(
|
rows = await fetch_analytics(
|
||||||
"""
|
"""
|
||||||
SELECT city_name, city_slug, lat, lon,
|
SELECT city_name, city_slug, lat, lon,
|
||||||
padel_venue_count, market_score, population
|
city_padel_venue_count AS padel_venue_count,
|
||||||
FROM serving.city_market_profile
|
market_score, opportunity_score, population
|
||||||
WHERE country_slug = ?
|
FROM serving.location_profiles
|
||||||
ORDER BY padel_venue_count DESC
|
WHERE country_slug = ? AND city_slug IS NOT NULL
|
||||||
|
ORDER BY city_padel_venue_count DESC
|
||||||
LIMIT 200
|
LIMIT 200
|
||||||
""",
|
""",
|
||||||
[country_slug],
|
[country_slug],
|
||||||
@@ -102,9 +105,10 @@ async def opportunity(country_slug: str):
|
|||||||
rows = await fetch_analytics(
|
rows = await fetch_analytics(
|
||||||
"""
|
"""
|
||||||
SELECT location_name, location_slug, lat, lon,
|
SELECT location_name, location_slug, lat, lon,
|
||||||
opportunity_score, nearest_padel_court_km,
|
opportunity_score, market_score,
|
||||||
|
nearest_padel_court_km,
|
||||||
padel_venue_count, population
|
padel_venue_count, population
|
||||||
FROM serving.location_opportunity_profile
|
FROM serving.location_profiles
|
||||||
WHERE country_slug = ? AND opportunity_score > 0
|
WHERE country_slug = ? AND opportunity_score > 0
|
||||||
ORDER BY opportunity_score DESC
|
ORDER BY opportunity_score DESC
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ async def opportunity_map():
|
|||||||
abort(404)
|
abort(404)
|
||||||
countries = await fetch_analytics("""
|
countries = await fetch_analytics("""
|
||||||
SELECT DISTINCT country_slug, country_name_en
|
SELECT DISTINCT country_slug, country_name_en
|
||||||
FROM serving.city_market_profile
|
FROM serving.location_profiles
|
||||||
|
WHERE city_slug IS NOT NULL
|
||||||
ORDER BY country_name_en
|
ORDER BY country_name_en
|
||||||
""")
|
""")
|
||||||
return await render_template("opportunity_map.html", countries=countries)
|
return await render_template("opportunity_map.html", countries=countries)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ def serving_meta_dir():
|
|||||||
meta = {
|
meta = {
|
||||||
"exported_at_utc": "2026-02-25T08:30:00+00:00",
|
"exported_at_utc": "2026-02-25T08:30:00+00:00",
|
||||||
"tables": {
|
"tables": {
|
||||||
"city_market_profile": {"row_count": 612},
|
"location_profiles": {"row_count": 612},
|
||||||
"planner_defaults": {"row_count": 612},
|
"planner_defaults": {"row_count": 612},
|
||||||
"pseo_city_costs_de": {"row_count": 487},
|
"pseo_city_costs_de": {"row_count": 487},
|
||||||
},
|
},
|
||||||
@@ -78,16 +78,16 @@ def serving_meta_dir():
|
|||||||
# ── Schema + query mocks ──────────────────────────────────────────────────────
|
# ── Schema + query mocks ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
_MOCK_SCHEMA_ROWS = [
|
_MOCK_SCHEMA_ROWS = [
|
||||||
{"table_name": "city_market_profile", "column_name": "city_slug", "data_type": "VARCHAR", "ordinal_position": 1},
|
{"table_name": "location_profiles", "column_name": "city_slug", "data_type": "VARCHAR", "ordinal_position": 1},
|
||||||
{"table_name": "city_market_profile", "column_name": "country_code", "data_type": "VARCHAR", "ordinal_position": 2},
|
{"table_name": "location_profiles", "column_name": "country_code", "data_type": "VARCHAR", "ordinal_position": 2},
|
||||||
{"table_name": "city_market_profile", "column_name": "marktreife_score", "data_type": "DOUBLE", "ordinal_position": 3},
|
{"table_name": "location_profiles", "column_name": "market_score", "data_type": "DOUBLE", "ordinal_position": 3},
|
||||||
{"table_name": "planner_defaults", "column_name": "city_slug", "data_type": "VARCHAR", "ordinal_position": 1},
|
{"table_name": "planner_defaults", "column_name": "city_slug", "data_type": "VARCHAR", "ordinal_position": 1},
|
||||||
]
|
]
|
||||||
|
|
||||||
_MOCK_TABLE_EXISTS = [{"1": 1}]
|
_MOCK_TABLE_EXISTS = [{"1": 1}]
|
||||||
_MOCK_SAMPLE_ROWS = [
|
_MOCK_SAMPLE_ROWS = [
|
||||||
{"city_slug": "berlin", "country_code": "DE", "marktreife_score": 82.5},
|
{"city_slug": "berlin", "country_code": "DE", "market_score": 82.5},
|
||||||
{"city_slug": "munich", "country_code": "DE", "marktreife_score": 77.0},
|
{"city_slug": "munich", "country_code": "DE", "market_score": 77.0},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ def _make_fetch_analytics_mock(schema=True):
|
|||||||
return [r for r in _MOCK_SCHEMA_ROWS if r["table_name"] == params[0]]
|
return [r for r in _MOCK_SCHEMA_ROWS if r["table_name"] == params[0]]
|
||||||
if "information_schema.columns" in sql:
|
if "information_schema.columns" in sql:
|
||||||
return _MOCK_SCHEMA_ROWS
|
return _MOCK_SCHEMA_ROWS
|
||||||
if "city_market_profile" in sql:
|
if "location_profiles" in sql:
|
||||||
return _MOCK_SAMPLE_ROWS
|
return _MOCK_SAMPLE_ROWS
|
||||||
return []
|
return []
|
||||||
return _mock
|
return _mock
|
||||||
@@ -162,7 +162,7 @@ async def test_pipeline_overview(admin_client, state_db_dir, serving_meta_dir):
|
|||||||
resp = await admin_client.get("/admin/pipeline/overview")
|
resp = await admin_client.get("/admin/pipeline/overview")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = await resp.get_data(as_text=True)
|
data = await resp.get_data(as_text=True)
|
||||||
assert "city_market_profile" in data
|
assert "location_profiles" in data
|
||||||
assert "612" in data # row count from serving meta
|
assert "612" in data # row count from serving meta
|
||||||
|
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@ async def test_pipeline_catalog(admin_client, serving_meta_dir):
|
|||||||
resp = await admin_client.get("/admin/pipeline/catalog")
|
resp = await admin_client.get("/admin/pipeline/catalog")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = await resp.get_data(as_text=True)
|
data = await resp.get_data(as_text=True)
|
||||||
assert "city_market_profile" in data
|
assert "location_profiles" in data
|
||||||
assert "612" in data # row count from serving meta
|
assert "612" in data # row count from serving meta
|
||||||
|
|
||||||
|
|
||||||
@@ -322,7 +322,7 @@ async def test_pipeline_catalog(admin_client, serving_meta_dir):
|
|||||||
async def test_pipeline_table_detail(admin_client):
|
async def test_pipeline_table_detail(admin_client):
|
||||||
"""Table detail returns columns and sample rows."""
|
"""Table detail returns columns and sample rows."""
|
||||||
with patch("padelnomics.analytics.fetch_analytics", side_effect=_make_fetch_analytics_mock()):
|
with patch("padelnomics.analytics.fetch_analytics", side_effect=_make_fetch_analytics_mock()):
|
||||||
resp = await admin_client.get("/admin/pipeline/catalog/city_market_profile")
|
resp = await admin_client.get("/admin/pipeline/catalog/location_profiles")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = await resp.get_data(as_text=True)
|
data = await resp.get_data(as_text=True)
|
||||||
assert "city_slug" in data
|
assert "city_slug" in data
|
||||||
@@ -362,7 +362,7 @@ async def test_pipeline_query_editor_loads(admin_client):
|
|||||||
data = await resp.get_data(as_text=True)
|
data = await resp.get_data(as_text=True)
|
||||||
assert "query-editor" in data
|
assert "query-editor" in data
|
||||||
assert "schema-panel" in data
|
assert "schema-panel" in data
|
||||||
assert "city_market_profile" in data
|
assert "location_profiles" in data
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -380,7 +380,7 @@ async def test_pipeline_query_execute_valid(admin_client):
|
|||||||
with patch("padelnomics.analytics.execute_user_query", new_callable=AsyncMock, return_value=mock_result):
|
with patch("padelnomics.analytics.execute_user_query", new_callable=AsyncMock, return_value=mock_result):
|
||||||
resp = await admin_client.post(
|
resp = await admin_client.post(
|
||||||
"/admin/pipeline/query/execute",
|
"/admin/pipeline/query/execute",
|
||||||
form={"csrf_token": "test", "sql": "SELECT city_slug, country_code FROM serving.city_market_profile"},
|
form={"csrf_token": "test", "sql": "SELECT city_slug, country_code FROM serving.location_profiles"},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = await resp.get_data(as_text=True)
|
data = await resp.get_data(as_text=True)
|
||||||
@@ -397,7 +397,7 @@ async def test_pipeline_query_execute_blocked_keyword(admin_client):
|
|||||||
with patch("padelnomics.analytics.execute_user_query", new_callable=AsyncMock) as mock_q:
|
with patch("padelnomics.analytics.execute_user_query", new_callable=AsyncMock) as mock_q:
|
||||||
resp = await admin_client.post(
|
resp = await admin_client.post(
|
||||||
"/admin/pipeline/query/execute",
|
"/admin/pipeline/query/execute",
|
||||||
form={"csrf_token": "test", "sql": "DROP TABLE serving.city_market_profile"},
|
form={"csrf_token": "test", "sql": "DROP TABLE serving.location_profiles"},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
data = await resp.get_data(as_text=True)
|
data = await resp.get_data(as_text=True)
|
||||||
@@ -532,8 +532,8 @@ def test_load_serving_meta(serving_meta_dir):
|
|||||||
with patch.object(pipeline_mod, "_SERVING_DUCKDB_PATH", str(Path(serving_meta_dir) / "analytics.duckdb")):
|
with patch.object(pipeline_mod, "_SERVING_DUCKDB_PATH", str(Path(serving_meta_dir) / "analytics.duckdb")):
|
||||||
meta = pipeline_mod._load_serving_meta()
|
meta = pipeline_mod._load_serving_meta()
|
||||||
assert meta is not None
|
assert meta is not None
|
||||||
assert "city_market_profile" in meta["tables"]
|
assert "location_profiles" in meta["tables"]
|
||||||
assert meta["tables"]["city_market_profile"]["row_count"] == 612
|
assert meta["tables"]["location_profiles"]["row_count"] == 612
|
||||||
|
|
||||||
|
|
||||||
def test_load_serving_meta_missing():
|
def test_load_serving_meta_missing():
|
||||||
|
|||||||
Reference in New Issue
Block a user