diff --git a/web/src/padelnomics/admin/pipeline_routes.py b/web/src/padelnomics/admin/pipeline_routes.py index f477e85..3189da0 100644 --- a/web/src/padelnomics/admin/pipeline_routes.py +++ b/web/src/padelnomics/admin/pipeline_routes.py @@ -111,13 +111,12 @@ _DAG: dict[str, list[str]] = { "fct_daily_availability": ["fct_availability_slot", "dim_venue_capacity"], # Serving "venue_pricing_benchmarks": ["fct_daily_availability"], - "city_market_profile": ["dim_cities", "venue_pricing_benchmarks"], - "planner_defaults": ["venue_pricing_benchmarks", "city_market_profile"], - "location_opportunity_profile": ["dim_locations"], + "location_profiles": ["dim_locations", "dim_cities", "venue_pricing_benchmarks"], + "planner_defaults": ["venue_pricing_benchmarks", "location_profiles"], "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"], } diff --git a/web/src/padelnomics/admin/templates/admin/partials/pipeline_query.html b/web/src/padelnomics/admin/templates/admin/partials/pipeline_query.html index 212b423..1a8087d 100644 --- a/web/src/padelnomics/admin/templates/admin/partials/pipeline_query.html +++ b/web/src/padelnomics/admin/templates/admin/partials/pipeline_query.html @@ -171,7 +171,7 @@ autocomplete="off" autocorrect="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" >
diff --git a/web/src/padelnomics/analytics.py b/web/src/padelnomics/analytics.py index b00c955..9333f57 100644 --- a/web/src/padelnomics/analytics.py +++ b/web/src/padelnomics/analytics.py @@ -13,7 +13,7 @@ Usage: 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 logging diff --git a/web/src/padelnomics/api.py b/web/src/padelnomics/api.py index 8db5bae..5ba4fb9 100644 --- a/web/src/padelnomics/api.py +++ b/web/src/padelnomics/api.py @@ -32,12 +32,14 @@ async def countries(): rows = await fetch_analytics(""" SELECT country_code, country_name_en, country_slug, 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(opportunity_score), 1) AS avg_opportunity_score, 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 - HAVING SUM(padel_venue_count) > 0 + HAVING SUM(city_padel_venue_count) > 0 ORDER BY total_venues DESC """) return jsonify(rows), 200, _CACHE_HEADERS @@ -51,10 +53,11 @@ async def country_cities(country_slug: str): rows = await fetch_analytics( """ SELECT city_name, city_slug, lat, lon, - padel_venue_count, market_score, population - FROM serving.city_market_profile - WHERE country_slug = ? - ORDER BY padel_venue_count DESC + city_padel_venue_count AS padel_venue_count, + market_score, opportunity_score, population + FROM serving.location_profiles + WHERE country_slug = ? AND city_slug IS NOT NULL + ORDER BY city_padel_venue_count DESC LIMIT 200 """, [country_slug], @@ -102,9 +105,10 @@ async def opportunity(country_slug: str): rows = await fetch_analytics( """ 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 - FROM serving.location_opportunity_profile + FROM serving.location_profiles WHERE country_slug = ? AND opportunity_score > 0 ORDER BY opportunity_score DESC LIMIT 500 diff --git a/web/src/padelnomics/public/routes.py b/web/src/padelnomics/public/routes.py index 018bf74..1a8ba03 100644 --- a/web/src/padelnomics/public/routes.py +++ b/web/src/padelnomics/public/routes.py @@ -80,7 +80,8 @@ async def opportunity_map(): abort(404) countries = await fetch_analytics(""" 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 """) return await render_template("opportunity_map.html", countries=countries) diff --git a/web/tests/test_pipeline.py b/web/tests/test_pipeline.py index bdd4b96..8f4ffec 100644 --- a/web/tests/test_pipeline.py +++ b/web/tests/test_pipeline.py @@ -64,7 +64,7 @@ def serving_meta_dir(): meta = { "exported_at_utc": "2026-02-25T08:30:00+00:00", "tables": { - "city_market_profile": {"row_count": 612}, + "location_profiles": {"row_count": 612}, "planner_defaults": {"row_count": 612}, "pseo_city_costs_de": {"row_count": 487}, }, @@ -78,16 +78,16 @@ def serving_meta_dir(): # ── Schema + query mocks ────────────────────────────────────────────────────── _MOCK_SCHEMA_ROWS = [ - {"table_name": "city_market_profile", "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": "city_market_profile", "column_name": "marktreife_score", "data_type": "DOUBLE", "ordinal_position": 3}, + {"table_name": "location_profiles", "column_name": "city_slug", "data_type": "VARCHAR", "ordinal_position": 1}, + {"table_name": "location_profiles", "column_name": "country_code", "data_type": "VARCHAR", "ordinal_position": 2}, + {"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}, ] _MOCK_TABLE_EXISTS = [{"1": 1}] _MOCK_SAMPLE_ROWS = [ - {"city_slug": "berlin", "country_code": "DE", "marktreife_score": 82.5}, - {"city_slug": "munich", "country_code": "DE", "marktreife_score": 77.0}, + {"city_slug": "berlin", "country_code": "DE", "market_score": 82.5}, + {"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]] if "information_schema.columns" in sql: return _MOCK_SCHEMA_ROWS - if "city_market_profile" in sql: + if "location_profiles" in sql: return _MOCK_SAMPLE_ROWS return [] 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") assert resp.status_code == 200 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 @@ -314,7 +314,7 @@ async def test_pipeline_catalog(admin_client, serving_meta_dir): resp = await admin_client.get("/admin/pipeline/catalog") assert resp.status_code == 200 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 @@ -322,7 +322,7 @@ async def test_pipeline_catalog(admin_client, serving_meta_dir): async def test_pipeline_table_detail(admin_client): """Table detail returns columns and sample rows.""" 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 data = await resp.get_data(as_text=True) 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) assert "query-editor" in data assert "schema-panel" in data - assert "city_market_profile" in data + assert "location_profiles" in data @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): resp = await admin_client.post( "/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 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: resp = await admin_client.post( "/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 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")): meta = pipeline_mod._load_serving_meta() assert meta is not None - assert "city_market_profile" in meta["tables"] - assert meta["tables"]["city_market_profile"]["row_count"] == 612 + assert "location_profiles" in meta["tables"] + assert meta["tables"]["location_profiles"]["row_count"] == 612 def test_load_serving_meta_missing():