feat(seo): expand city coverage to 40 cities + DuckDB refresh script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-22 00:02:13 +01:00
3 changed files with 818 additions and 0 deletions

View File

@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
### Added
- Expanded programmatic SEO city coverage from 18 to 40 cities (+22 cities across ES, FR,
IT, NL, AT, CH, SE, PT, BE, AE, AU, IE) — generates 80 articles (40 cities × EN + DE)
- `scripts/refresh_from_daas.py`: syncs template_data rows from DuckDB `planner_defaults`
serving table; supports `--dry-run` and `--generate` flags; graceful no-op when DuckDB unavailable
### Added
- `analytics.py`: DuckDB read-only reader (`open_analytics_db`, `close_analytics_db`,
`fetch_analytics`) registered in app lifecycle (startup/shutdown)

View File

@@ -0,0 +1,208 @@
"""
Refresh template_data rows from DuckDB analytics serving layer.
Reads per-city market data from the `padelnomics.planner_defaults` serving table
and overwrites matching static values in `template_data.data_json`. This keeps
article financial model inputs in sync with the real-world data pipeline output.
Usage:
# Dry-run — show what would change without writing:
uv run python -m padelnomics.scripts.refresh_from_daas --dry-run
# Apply updates:
uv run python -m padelnomics.scripts.refresh_from_daas
# Also re-generate articles after updating:
uv run python -m padelnomics.scripts.refresh_from_daas --generate
Graceful degradation:
- If DUCKDB_PATH is unset or the file doesn't exist, exits 0 with a warning.
- Cities not found in the analytics layer are left unchanged.
- Only fields with non-null values in the analytics layer are updated.
Fields mapped (DuckDB → data_json camelCase key):
rate_peak → ratePeak
rate_off_peak → rateOffPeak
court_cost_dbl → courtCostDbl
court_cost_sgl → courtCostSgl
rent_sqm → rentSqm
insurance → insurance
electricity → electricity
maintenance → maintenance
marketing → marketing
"""
import argparse
import json
import os
import sqlite3
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
DATABASE_PATH = os.getenv("DATABASE_PATH", "data/app.db")
DUCKDB_PATH = os.getenv("DUCKDB_PATH", "data/lakehouse.duckdb")
# DuckDB column → data_json camelCase key
_FIELD_MAP: dict[str, str] = {
"rate_peak": "ratePeak",
"rate_off_peak": "rateOffPeak",
"court_cost_dbl": "courtCostDbl",
"court_cost_sgl": "courtCostSgl",
"rent_sqm": "rentSqm",
"insurance": "insurance",
"electricity": "electricity",
"maintenance": "maintenance",
"marketing": "marketing",
}
def _load_analytics(city_slugs: list[str]) -> dict[str, dict]:
"""Query DuckDB for planner_defaults rows matching the given city slugs.
Returns dict of city_slug → {camelCase key: value}.
Returns {} if DuckDB is unavailable.
"""
path = Path(DUCKDB_PATH)
if not path.exists():
print(f" [analytics] DuckDB not found at {path} — skipping analytics refresh.")
return {}
try:
import duckdb
except ImportError:
print(" [analytics] duckdb not installed — skipping analytics refresh.")
return {}
result: dict[str, dict] = {}
try:
conn = duckdb.connect(str(path), read_only=True)
placeholders = ", ".join(["?"] * len(city_slugs))
rows = conn.execute(
f"SELECT * FROM padelnomics.planner_defaults WHERE city_slug IN ({placeholders})",
city_slugs,
).fetchall()
cols = [d[0] for d in conn.description]
conn.close()
for row in rows:
row_dict = dict(zip(cols, row))
slug = row_dict["city_slug"]
overrides: dict = {}
for col, key in _FIELD_MAP.items():
val = row_dict.get(col)
if val is not None:
overrides[key] = round(float(val))
result[slug] = overrides
except Exception as exc:
print(f" [analytics] DuckDB query failed: {exc}")
return result
def refresh(dry_run: bool = False) -> int:
"""Update template_data rows from analytics. Returns count of rows updated."""
conn = sqlite3.connect(DATABASE_PATH)
conn.row_factory = sqlite3.Row
# Fetch all template_data rows that have a city_slug field
rows = conn.execute(
"SELECT id, template_id, data_json FROM template_data"
).fetchall()
city_slug_to_ids: dict[str, list[int]] = {}
for row in rows:
try:
data = json.loads(row["data_json"])
except (json.JSONDecodeError, TypeError):
continue
slug = data.get("city_slug", "")
if slug:
city_slug_to_ids.setdefault(slug, []).append(row["id"])
if not city_slug_to_ids:
print("No template_data rows with city_slug found.")
conn.close()
return 0
analytics = _load_analytics(list(city_slug_to_ids.keys()))
if not analytics:
print("No analytics data found — nothing to update.")
conn.close()
return 0
updated = 0
for slug, overrides in analytics.items():
ids = city_slug_to_ids.get(slug, [])
if not ids:
continue
for row_id in ids:
row = conn.execute(
"SELECT data_json FROM template_data WHERE id = ?", (row_id,)
).fetchone()
if not row:
continue
data = json.loads(row["data_json"])
changed = {k: v for k, v in overrides.items() if data.get(k) != v}
if not changed:
continue
data.update(overrides)
if dry_run:
print(f" [dry-run] id={row_id} city_slug={slug}: {changed}")
else:
conn.execute(
"UPDATE template_data SET data_json = ?, updated_at = datetime('now') WHERE id = ?",
(json.dumps(data), row_id),
)
print(f" Updated id={row_id} city_slug={slug}: {list(changed.keys())}")
updated += 1
if not dry_run:
conn.commit()
conn.close()
return updated
def _trigger_generation() -> None:
"""Trigger article generation via admin endpoint (requires running app)."""
import urllib.request
admin_key = os.getenv("ADMIN_API_KEY", "")
base_url = os.getenv("BASE_URL", "http://localhost:5000")
url = f"{base_url}/admin/generate-all"
assert admin_key, "ADMIN_API_KEY env var required to trigger generation"
req = urllib.request.Request(
url,
method="POST",
headers={"X-Admin-Key": admin_key},
)
with urllib.request.urlopen(req, timeout=120) as resp:
print(f" Generation triggered: HTTP {resp.status}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dry-run", action="store_true",
help="Print changes without writing to DB")
parser.add_argument("--generate", action="store_true",
help="Trigger article re-generation after updating")
args = parser.parse_args()
print(f"{'[DRY RUN] ' if args.dry_run else ''}Refreshing template_data from DuckDB…")
count = refresh(dry_run=args.dry_run)
print(f"{'Would update' if args.dry_run else 'Updated'} {count} rows.")
if args.generate and count > 0 and not args.dry_run:
print("Triggering article generation…")
_trigger_generation()
if __name__ == "__main__":
main()

View File

@@ -705,6 +705,609 @@ CITIES = [
"rentSqm": 4, "electricity": 630, "heating": 440,
"staff": 2100, "permitsCompliance": 10000, "utilTarget": 38,
},
# ------------------------------------------------------------------ Spain
{
"city": "Madrid",
"city_slug_en": "madrid",
"city_slug_de": "madrid",
"country": "ES",
"country_name_en": "Spain",
"country_name_de": "Spanien",
"region": "Europe",
"population": "3.4M",
"padel_context_en": (
"Madrid is the world's densest padel market, with over 5,000 courts "
"nationwide and the sport embedded in everyday culture. High utilisation "
"rates and a deeply padel-literate customer base make Madrid one of the "
"best-validated markets for new investment."
),
"padel_context_de": (
"Madrid ist der weltweit dichteste Padelmarkt mit über 5.000 Courts "
"und dem Sport als fester Alltagskultur. Hohe Auslastungsraten und "
"eine padelerfahrene Kundschaft machen Madrid zu einem der "
"bestvalidiertesten Märkte für neue Investitionen."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 5, "sglCourts": 2,
"ratePeak": 44, "rateOffPeak": 30,
"rentSqm": 6, "electricity": 580, "heating": 50,
"staff": 2200, "permitsCompliance": 25000, "utilTarget": 60,
},
{
"city": "Barcelona",
"city_slug_en": "barcelona",
"city_slug_de": "barcelona",
"country": "ES",
"country_name_en": "Spain",
"country_name_de": "Spanien",
"region": "Europe",
"population": "1.6M",
"padel_context_en": (
"Barcelona combines a mature padel culture with year-round outdoor "
"conditions and strong tourism demand. Premium zones near the seafront "
"command higher rates, while suburban clubs operate at lower cost with "
"equally strong fill rates."
),
"padel_context_de": (
"Barcelona verbindet eine reife Padelkultur mit ganzjährig "
"günstigen Outdoor-Bedingungen und starker Tourismusnachfrage. "
"Premium-Zonen nahe der Küste erzielen höhere Preise, während "
"Vorortclubs bei niedrigeren Kosten gleichstarke Belegung aufweisen."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 5, "sglCourts": 2,
"ratePeak": 47, "rateOffPeak": 32,
"rentSqm": 8, "electricity": 590, "heating": 0,
"staff": 2300, "permitsCompliance": 22000, "utilTarget": 60,
},
{
"city": "Valencia",
"city_slug_en": "valencia",
"city_slug_de": "valencia",
"country": "ES",
"country_name_en": "Spain",
"country_name_de": "Spanien",
"region": "Europe",
"population": "800K",
"padel_context_en": (
"Valencia offers a compelling cost-to-utilisation ratio. Commercial "
"rents are 3040% below Madrid, warm winters allow year-round outdoor "
"play, and the padel culture is well established — making it one of "
"Spain's most attractive second-tier markets."
),
"padel_context_de": (
"Valencia bietet ein überzeugendes Kosten-Auslastungs-Verhältnis. "
"Gewerbemieten liegen 3040% unter Madrid, warme Winter ermöglichen "
"ganzjähriges Outdoor-Spiel, und die Padelkultur ist fest etabliert — "
"einer der attraktivsten spanischen Zweitplatzmärkte."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 38, "rateOffPeak": 26,
"rentSqm": 5, "electricity": 550, "heating": 0,
"staff": 1800, "permitsCompliance": 18000, "utilTarget": 55,
},
{
"city": "Seville",
"city_slug_en": "seville",
"city_slug_de": "sevilla",
"country": "ES",
"country_name_en": "Spain",
"country_name_de": "Spanien",
"region": "Europe",
"population": "690K",
"padel_context_en": (
"Seville's hot, dry climate enables year-round outdoor padel, dramatically "
"lowering operating costs versus indoor-only markets. Strong local sports "
"culture and lower land prices combine to produce attractive returns for "
"early-mover operators."
),
"padel_context_de": (
"Sevillas heißes, trockenes Klima ermöglicht ganzjähriges Outdoor-Padel "
"und senkt die Betriebskosten gegenüber reinen Innenmarktstandorten erheblich. "
"Starke lokale Sportkultur und niedrigere Grundstückspreise ergeben "
"attraktive Renditen für Erstanbieter."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 35, "rateOffPeak": 24,
"rentSqm": 4, "electricity": 520, "heating": 0,
"staff": 1600, "permitsCompliance": 20000, "utilTarget": 52,
},
{
"city": "Málaga",
"city_slug_en": "malaga",
"city_slug_de": "malaga",
"country": "ES",
"country_name_en": "Spain",
"country_name_de": "Spanien",
"region": "Europe",
"population": "580K",
"padel_context_en": (
"Málaga's status as a major expat and tourism hub creates dual revenue "
"streams: local membership and high-rate tourist bookings. The city's "
"tech scene and growing digital-nomad community add a further demographic "
"of high-frequency casual players."
),
"padel_context_de": (
"Málagas Status als bedeutendes Expat- und Tourismuszentrum schafft "
"zwei Einnahmequellen: lokale Mitgliedschaften und hochpreisige Touristenbuchungen. "
"Die Tech-Szene und wachsende Digital-Nomad-Gemeinschaft der Stadt schaffen "
"eine weitere Demografie hochfrequenter Gelegenheitsspieler."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 42, "rateOffPeak": 28,
"rentSqm": 5, "electricity": 540, "heating": 0,
"staff": 1700, "permitsCompliance": 18000, "utilTarget": 53,
},
# ------------------------------------------------------------------ France
{
"city": "Paris",
"city_slug_en": "paris",
"city_slug_de": "paris",
"country": "FR",
"country_name_en": "France",
"country_name_de": "Frankreich",
"region": "Europe",
"population": "2.1M (metro 12M)",
"padel_context_en": (
"Paris is France's largest and fastest-growing padel market, with demand "
"significantly outpacing supply. High disposable incomes, an appetite for "
"premium sports experiences, and tight commercial space keep prices elevated "
"but support strong utilisation."
),
"padel_context_de": (
"Paris ist Frankreichs größter und am schnellsten wachsender Padelmarkt, "
"mit deutlich überschüssiger Nachfrage. Hohe verfügbare Einkommen, "
"Nachfrage nach Premium-Sporterfahrungen und knappes Gewerbeflächenangebot "
"halten die Preise hoch und stützen eine starke Auslastung."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 58, "rateOffPeak": 40,
"rentSqm": 12, "electricity": 620, "heating": 330,
"staff": 3000, "permitsCompliance": 15000, "utilTarget": 47,
},
{
"city": "Lyon",
"city_slug_en": "lyon",
"city_slug_de": "lyon",
"country": "FR",
"country_name_en": "France",
"country_name_de": "Frankreich",
"region": "Europe",
"population": "520K (metro 2.3M)",
"padel_context_en": (
"Lyon has emerged as one of France's most active provincial padel markets, "
"backed by a large student and professional population. Lower rents than "
"Paris and a growing club network make it an attractive second-city opportunity."
),
"padel_context_de": (
"Lyon hat sich als einer der aktivsten provinziellen Padelmärkte Frankreichs "
"etabliert, getragen von einer großen Studenten- und Berufsschicht. "
"Günstigere Mieten als Paris und ein wachsendes Clubnetzwerk machen "
"es zu einer attraktiven Zweitstadtchance."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 42, "rateOffPeak": 30,
"rentSqm": 6, "electricity": 560, "heating": 280,
"staff": 2000, "permitsCompliance": 12000, "utilTarget": 40,
},
{
"city": "Marseille",
"city_slug_en": "marseille",
"city_slug_de": "marseille",
"country": "FR",
"country_name_en": "France",
"country_name_de": "Frankreich",
"region": "Europe",
"population": "870K",
"padel_context_en": (
"Marseille's Mediterranean climate reduces heating costs significantly "
"and extends the viable outdoor season. As France's second city by size, "
"it offers deep demographic reach at commercial rents well below Paris or Lyon."
),
"padel_context_de": (
"Marseilles Mittelmeerklima senkt die Heizkosten erheblich und verlängert "
"die nutzbare Outdoor-Saison. Als zweitgrößte Stadt Frankreichs bietet sie "
"demographische Tiefe bei Gewerbemieten weit unter Paris oder Lyon."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 38, "rateOffPeak": 27,
"rentSqm": 5, "electricity": 540, "heating": 100,
"staff": 1700, "permitsCompliance": 12000, "utilTarget": 40,
},
# ------------------------------------------------------------------ Italy
{
"city": "Milan",
"city_slug_en": "milan",
"city_slug_de": "mailand",
"country": "IT",
"country_name_en": "Italy",
"country_name_de": "Italien",
"region": "Europe",
"population": "1.4M (metro 3.2M)",
"padel_context_en": (
"Milan leads Italy's padel expansion, fuelled by high disposable incomes "
"and a business culture that treats sport as a social currency. Demand from "
"corporate accounts is particularly strong, with firms booking courts "
"for team activities and client entertainment."
),
"padel_context_de": (
"Mailand führt Italiens Padelexpansion an, angetrieben durch hohes "
"verfügbares Einkommen und eine Geschäftskultur, die Sport als soziale "
"Währung betrachtet. Die Nachfrage von Firmenkonten ist besonders stark, "
"da Unternehmen Courts für Teamaktivitäten und Kundenunterhaltung buchen."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 52, "rateOffPeak": 36,
"rentSqm": 9, "electricity": 730, "heating": 400,
"staff": 2500, "permitsCompliance": 18000, "utilTarget": 46,
},
{
"city": "Rome",
"city_slug_en": "rome",
"city_slug_de": "rom",
"country": "IT",
"country_name_en": "Italy",
"country_name_de": "Italien",
"region": "Europe",
"population": "2.8M",
"padel_context_en": (
"Rome's mild winters and vast population base underpin a rapidly growing "
"padel market. Outdoor courts operate profitably year-round, reducing "
"CAPEX versus fully enclosed facilities. Central government facilities "
"and sports club conversions dominate early supply."
),
"padel_context_de": (
"Roms milde Winter und riesige Bevölkerungsbasis stützen einen schnell "
"wachsenden Padelmarkt. Outdoor-Courts arbeiten ganzjährig profitabel, "
"was den CAPEX gegenüber vollständig geschlossenen Anlagen reduziert. "
"Staatliche Anlagen und Sportclubumbauten dominieren das frühe Angebot."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 46, "rateOffPeak": 32,
"rentSqm": 7, "electricity": 700, "heating": 200,
"staff": 2200, "permitsCompliance": 20000, "utilTarget": 43,
},
# ---------------------------------------------- Netherlands
{
"city": "Amsterdam",
"city_slug_en": "amsterdam",
"city_slug_de": "amsterdam",
"country": "NL",
"country_name_en": "Netherlands",
"country_name_de": "Niederlande",
"region": "Europe",
"population": "920K",
"padel_context_en": (
"Amsterdam combines a sports-active, internationally minded population "
"with one of Europe's highest purchasing powers. The city's compact geography "
"and limited available space push operators toward efficient indoor multi-court "
"venues, justifying premium pricing."
),
"padel_context_de": (
"Amsterdam verbindet eine sportaktive, international ausgerichtete Bevölkerung "
"mit einer der höchsten Kaufkräfte Europas. Die kompakte Geografie und begrenzte "
"Verfügbarkeit von Räumlichkeiten drängen Betreiber zu effizienten Indoor-"
"Mehrcourt-Anlagen und rechtfertigen Premium-Preise."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 54, "rateOffPeak": 38,
"rentSqm": 10, "electricity": 660, "heating": 460,
"staff": 2800, "permitsCompliance": 10000, "utilTarget": 43,
},
# ----------------------------------------------------------------- Austria
{
"city": "Vienna",
"city_slug_en": "vienna",
"city_slug_de": "wien",
"country": "AT",
"country_name_en": "Austria",
"country_name_de": "Österreich",
"region": "Europe",
"population": "1.9M",
"padel_context_en": (
"Vienna is Austria's dominant padel market and one of Central Europe's "
"fastest-growing cities for the sport. A large English-speaking expat "
"community and high real incomes drive premium bookings, particularly "
"in the first and third districts."
),
"padel_context_de": (
"Wien ist Österreichs dominanter Padelmarkt und eine der am schnellsten "
"wachsenden Städte für den Sport in Mitteleuropa. Eine große englischsprachige "
"Expat-Gemeinschaft und hohe Realeinkommen treiben Premium-Buchungen an, "
"insbesondere im ersten und dritten Bezirk."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 50, "rateOffPeak": 35,
"rentSqm": 7, "electricity": 610, "heating": 510,
"staff": 2400, "permitsCompliance": 14000, "utilTarget": 42,
},
# --------------------------------------------------------------- Switzerland
{
"city": "Zurich",
"city_slug_en": "zurich",
"city_slug_de": "zuerich",
"country": "CH",
"country_name_en": "Switzerland",
"country_name_de": "Schweiz",
"region": "Europe",
"population": "440K (metro 1.4M)",
"padel_context_en": (
"Zurich is Europe's most expensive padel market, driven by the highest "
"average wages on the continent. Players expect premium facilities and "
"are willing to pay for them — average court rates are 6080% above "
"comparable German cities. Staff costs are the single largest operating "
"line item."
),
"padel_context_de": (
"Zürich ist Europas teuerster Padelmarkt, angetrieben durch die höchsten "
"Durchschnittslöhne des Kontinents. Spieler erwarten Premium-Anlagen und "
"sind bereit, dafür zu zahlen — durchschnittliche Courtpreise liegen 6080% "
"über vergleichbaren deutschen Städten. Personalkosten sind die größte "
"Einzelposition im Betrieb."
),
"currency_note_en": _EUR_NOTE_EN,
"currency_note_de": _EUR_NOTE_DE,
"dblCourts": 4, "sglCourts": 1,
"ratePeak": 75, "rateOffPeak": 52,
"rentSqm": 15, "electricity": 500, "heating": 480,
"staff": 4500, "permitsCompliance": 15000, "utilTarget": 45,
},
# ------------------------------------------------------------------ Sweden
{
"city": "Stockholm",
"city_slug_en": "stockholm",
"city_slug_de": "stockholm",
"country": "SE",
"country_name_en": "Sweden",
"country_name_de": "Schweden",
"region": "Europe",
"population": "980K",
"padel_context_en": (
"Stockholm has the highest padel court-per-capita ratio in the world outside "
"Spain. The sport is deeply embedded in Swedish corporate and social culture — "
"padel after work is a fixture for office teams. Indoor venues dominate "
"given the long winter, but cheap electricity (largely hydropower) keeps "
"operating costs manageable."
),
"padel_context_de": (
"Stockholm hat das weltweit höchste Padel-Court-pro-Kopf-Verhältnis außerhalb "
"Spaniens. Der Sport ist tief in der schwedischen Unternehmens- und Sozialkultur "
"verankert. Indoor-Anlagen dominieren aufgrund des langen Winters, aber günstige "
"Elektrizität (größtenteils Wasserkraft) hält die Betriebskosten beherrschbar."
),
"currency_note_en": _EUR_NOTE_EN,
"currency_note_de": _EUR_NOTE_DE,
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 62, "rateOffPeak": 44,
"rentSqm": 8, "electricity": 390, "heating": 650,
"staff": 3200, "permitsCompliance": 8000, "utilTarget": 46,
},
# ----------------------------------------------------------------- Portugal
{
"city": "Lisbon",
"city_slug_en": "lisbon",
"city_slug_de": "lissabon",
"country": "PT",
"country_name_en": "Portugal",
"country_name_de": "Portugal",
"region": "Europe",
"population": "550K (metro 2.8M)",
"padel_context_en": (
"Lisbon is one of Europe's fastest-growing padel markets, driven by an "
"influx of international residents and a warm climate that minimises "
"heating costs. Court rates are rising rapidly as demand outpaces supply, "
"making early-mover advantage particularly valuable."
),
"padel_context_de": (
"Lissabon ist einer der am schnellsten wachsenden Padelmärkte Europas, "
"angetrieben durch einen Zustrom internationaler Einwohner und ein warmes "
"Klima, das Heizkosten minimiert. Courtpreise steigen rapide, da die Nachfrage "
"das Angebot übersteigt — der Erstanbieter-Vorteil ist besonders wertvoll."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 38, "rateOffPeak": 26,
"rentSqm": 6, "electricity": 540, "heating": 80,
"staff": 1700, "permitsCompliance": 12000, "utilTarget": 52,
},
{
"city": "Porto",
"city_slug_en": "porto",
"city_slug_de": "porto",
"country": "PT",
"country_name_en": "Portugal",
"country_name_de": "Portugal",
"region": "Europe",
"population": "240K (metro 1.7M)",
"padel_context_en": (
"Porto offers the most accessible padel investment entry point in Western Europe. "
"Commercial rents are among the lowest of any major city in this report, "
"the growing tech sector is driving demand, and the mild Atlantic climate "
"supports year-round play without heating infrastructure."
),
"padel_context_de": (
"Porto bietet den günstigsten Padelinvestitions-Einstiegspunkt in Westeuropa. "
"Gewerbemieten gehören zu den niedrigsten aller Großstädte in diesem Bericht, "
"der wachsende Tech-Sektor treibt die Nachfrage, und das milde Atlantikklima "
"unterstützt ganzjähriges Spiel ohne Heizinfrastruktur."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 3, "sglCourts": 2,
"ratePeak": 32, "rateOffPeak": 22,
"rentSqm": 4, "electricity": 500, "heating": 50,
"staff": 1400, "permitsCompliance": 10000, "utilTarget": 48,
},
# ----------------------------------------------------------------- Belgium
{
"city": "Brussels",
"city_slug_en": "brussels",
"city_slug_de": "bruessel",
"country": "BE",
"country_name_en": "Belgium",
"country_name_de": "Belgien",
"region": "Europe",
"population": "1.2M (metro 2.1M)",
"padel_context_en": (
"Brussels benefits from a large international civil-servant and lobbying "
"community with high disposable incomes and a taste for premium leisure. "
"Belgium's electricity costs are among Europe's highest, making energy "
"efficiency a critical factor in site selection."
),
"padel_context_de": (
"Brüssel profitiert von einer großen internationalen Beamten- und "
"Lobbyisten-Gemeinschaft mit hohem verfügbarem Einkommen. Belgiens "
"Stromkosten gehören zu den höchsten Europas — Energieeffizienz ist "
"ein entscheidender Faktor bei der Standortwahl."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 50, "rateOffPeak": 35,
"rentSqm": 8, "electricity": 660, "heating": 450,
"staff": 2600, "permitsCompliance": 12000, "utilTarget": 41,
},
# ------------------------------------------------------------------- UAE
{
"city": "Dubai",
"city_slug_en": "dubai",
"city_slug_de": "dubai",
"country": "AE",
"country_name_en": "UAE",
"country_name_de": "Vereinigte Arabische Emirate",
"region": "Middle East",
"population": "3.7M",
"padel_context_en": (
"Dubai is the fastest-growing padel market in the Middle East, "
"with investment from European club operators and locally founded chains "
"expanding rapidly. Year-round outdoor conditions, no income tax, "
"and a high-earning expat population create exceptional unit economics. "
"AC costs replace heating but are heavily subsidised."
),
"padel_context_de": (
"Dubai ist der am schnellsten wachsende Padelmarkt im Nahen Osten, "
"mit Investitionen europäischer Clubbetreiber und lokaler Ketten. "
"Ganzjährige Outdoor-Bedingungen, keine Einkommensteuer und "
"eine gut verdienende Expat-Bevölkerung schaffen außergewöhnliche Unit Economics. "
"AC-Kosten ersetzen Heizung, sind aber stark subventioniert."
),
"currency_note_en": _EUR_NOTE_EN,
"currency_note_de": _EUR_NOTE_DE,
"dblCourts": 6, "sglCourts": 2,
"ratePeak": 85, "rateOffPeak": 58,
"rentSqm": 5, "electricity": 380, "heating": 0,
"staff": 4200, "permitsCompliance": 20000, "utilTarget": 58,
},
# ---------------------------------------------------------------- Australia
{
"city": "Sydney",
"city_slug_en": "sydney",
"city_slug_de": "sydney",
"country": "AU",
"country_name_en": "Australia",
"country_name_de": "Australien",
"region": "Asia Pacific",
"population": "5.3M",
"padel_context_en": (
"Sydney is Australia's largest and most developed padel market, "
"with supply ramping rapidly since 2022. High commercial rents and "
"electricity costs require strong utilisation to break even, but the "
"city's affluent coastal suburbs produce premium booking rates and "
"membership uptake above European benchmarks."
),
"padel_context_de": (
"Sydney ist Australiens größter und am weitesten entwickelter Padelmarkt, "
"mit rasant steigendem Angebot seit 2022. Hohe Gewerberaum- und Stromkosten "
"erfordern eine starke Auslastung, aber die wohlhabenden Küstenvorortee "
"der Stadt erzielen Premium-Buchungsraten über europäischen Benchmarks."
),
"currency_note_en": _EUR_NOTE_EN,
"currency_note_de": _EUR_NOTE_DE,
"dblCourts": 5, "sglCourts": 2,
"ratePeak": 68, "rateOffPeak": 48,
"rentSqm": 6, "electricity": 760, "heating": 80,
"staff": 3600, "permitsCompliance": 18000, "utilTarget": 50,
},
{
"city": "Melbourne",
"city_slug_en": "melbourne",
"city_slug_de": "melbourne",
"country": "AU",
"country_name_en": "Australia",
"country_name_de": "Australien",
"region": "Asia Pacific",
"population": "5.1M",
"padel_context_en": (
"Melbourne's four-seasons climate and strong indoor-sport infrastructure "
"make it well suited to padel. The city trails Sydney in court density "
"but benefits from somewhat lower commercial rents and a sports culture "
"that is quick to adopt new racket sports."
),
"padel_context_de": (
"Melbournes Vier-Jahreszeiten-Klima und starke Hallensport-Infrastruktur "
"machen es gut geeignet für Padel. Die Stadt liegt hinter Sydney in "
"der Court-Dichte, profitiert aber von etwas niedrigeren Gewerbemieten "
"und einer Sportkultur, die neue Racketsportarten schnell annimmt."
),
"currency_note_en": _EUR_NOTE_EN,
"currency_note_de": _EUR_NOTE_DE,
"dblCourts": 5, "sglCourts": 2,
"ratePeak": 62, "rateOffPeak": 44,
"rentSqm": 5, "electricity": 700, "heating": 100,
"staff": 3200, "permitsCompliance": 16000, "utilTarget": 47,
},
# ----------------------------------------------------------------- Ireland
{
"city": "Dublin",
"city_slug_en": "dublin",
"city_slug_de": "dublin",
"country": "IE",
"country_name_en": "Ireland",
"country_name_de": "Irland",
"region": "Europe",
"population": "1.4M",
"padel_context_en": (
"Dublin's booming tech sector and large young professional population "
"have driven rapid uptake of padel since 2022. Ireland's electricity "
"costs are among Europe's highest, significantly impacting operating "
"margins — energy-efficient site design is essential."
),
"padel_context_de": (
"Dublins boomender Tech-Sektor und eine große junge Berufsschicht "
"haben seit 2022 eine rasche Padeladoption angetrieben. Irlands Stromkosten "
"gehören zu den höchsten Europas und belasten die Betriebsmargen erheblich — "
"energieeffizientes Standortdesign ist unerlässlich."
),
"currency_note_en": "",
"currency_note_de": "",
"dblCourts": 4, "sglCourts": 2,
"ratePeak": 55, "rateOffPeak": 38,
"rentSqm": 9, "electricity": 800, "heating": 460,
"staff": 2900, "permitsCompliance": 12000, "utilTarget": 42,
},
]
# Calc fields that get passed to the financial model (must match DEFAULTS keys)