"""
Export command for generating static files.
"""
import json
import logging
from pathlib import Path
from typing import Optional, List, Dict, Any
from uuid import UUID
import typer
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
from jinja2 import Environment, FileSystemLoader
from ..services.supabase import SupabaseService
from ..services.tags import TagAggregationService
from ..services.images import ImageService
from ..config import get_settings
logger = logging.getLogger(__name__)
console = Console()
def export_command(
country: str,
city: Optional[str] = None,
output_dir: Optional[str] = None,
) -> None:
"""
Export static XML/HTML files for a country or city.
Generates company pages, city hubs, country hubs, and sitemaps
from the database content.
Args:
country: Country code (e.g., DE)
city: Optional specific city slug
output_dir: Optional output directory override
"""
console.print(f"\n[bold blue]Exporting files for {country}[/bold blue]\n")
settings = get_settings()
db = SupabaseService()
tags_service = TagAggregationService(db)
image_service = ImageService()
# Setup output directory
if output_dir:
export_path = Path(output_dir)
else:
export_path = settings.export_dir
export_path.mkdir(parents=True, exist_ok=True)
# Get country info
country_obj = db.get_country_by_code(country.upper())
if not country_obj:
console.print(f"[red]Country not found: {country}[/red]")
raise typer.Exit(1)
# Setup Jinja environment
jinja_env = _setup_jinja_environment(settings.templates_dir)
# Get cities to export
all_cities = db.list_cities(country_id=country_obj.id)
if city:
cities_to_export = [c for c in all_cities if c.slug == city]
if not cities_to_export:
console.print(f"[red]City not found: {city}[/red]")
raise typer.Exit(1)
else:
cities_to_export = all_cities
console.print(f"Exporting {len(cities_to_export)} cities...")
# Export each city
total_companies = 0
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
console=console,
) as progress:
task = progress.add_task("Exporting...", total=len(cities_to_export))
for city_obj in cities_to_export:
city_companies = _export_city(
db=db,
tags_service=tags_service,
image_service=image_service,
jinja_env=jinja_env,
country=country_obj,
city=city_obj,
export_path=export_path,
)
total_companies += city_companies
progress.advance(task)
# Export country hub
_export_country_hub(
db=db,
tags_service=tags_service,
jinja_env=jinja_env,
country=country_obj,
cities=cities_to_export,
export_path=export_path,
)
# Export sitemaps
_export_sitemaps(
db=db,
country=country_obj,
cities=cities_to_export,
export_path=export_path,
)
# Export tag JSON files for frontend filtering
_export_tag_json(
db=db,
tags_service=tags_service,
country=country_obj,
cities=cities_to_export,
export_path=export_path,
)
console.print(f"\n[bold green]Export complete![/bold green]")
console.print(f" - Companies: {total_companies}")
console.print(f" - Cities: {len(cities_to_export)}")
console.print(f" - Output: {export_path}")
def _setup_jinja_environment(templates_dir: Path) -> Environment:
"""Setup Jinja2 templating environment."""
# Create templates directory if it doesn't exist
templates_dir.mkdir(parents=True, exist_ok=True)
# Create default templates if they don't exist
_ensure_templates_exist(templates_dir)
return Environment(
loader=FileSystemLoader(str(templates_dir)),
autoescape=True,
)
def _ensure_templates_exist(templates_dir: Path) -> None:
"""Create default templates if they don't exist."""
# Company template
company_template = templates_dir / "company.xml"
if not company_template.exists():
company_template.write_text(_DEFAULT_COMPANY_TEMPLATE)
# City hub template
city_template = templates_dir / "city_hub.xml"
if not city_template.exists():
city_template.write_text(_DEFAULT_CITY_TEMPLATE)
# Country hub template
country_template = templates_dir / "country_hub.xml"
if not country_template.exists():
country_template.write_text(_DEFAULT_COUNTRY_TEMPLATE)
def _export_city(
db: SupabaseService,
tags_service: TagAggregationService,
image_service: ImageService,
jinja_env: Environment,
country,
city,
export_path: Path,
) -> int:
"""Export all files for a city."""
# Create city directory
city_path = export_path / country.slug / city.slug
city_path.mkdir(parents=True, exist_ok=True)
# Get companies
companies = db.list_companies(
city_id=city.id,
status="content_generated",
limit=500,
)
# Export each company
company_template = jinja_env.get_template("company.xml")
for company in companies:
company_tags = db.get_company_tags(company.id)
context = {
"company": company,
"city": city,
"country": country,
"tags": company_tags,
"services": [t for t in company_tags if hasattr(t, "category") and t.category == "service"],
"industries": [t for t in company_tags if hasattr(t, "category") and t.category == "industry"],
"modules": [t for t in company_tags if hasattr(t, "category") and t.category == "module"],
}
content = company_template.render(**context)
company_file = city_path / f"{company.slug}.html"
company_file.write_text(content, encoding="utf-8")
# Export city hub
city_tags = tags_service.get_city_tags_by_category(city.id)
hub_template = jinja_env.get_template("city_hub.xml")
hub_context = {
"city": city,
"country": country,
"companies": companies,
"company_count": len(companies),
"tags": city_tags,
}
hub_content = hub_template.render(**hub_context)
hub_file = city_path / "index.html"
hub_file.write_text(hub_content, encoding="utf-8")
return len(companies)
def _export_country_hub(
db: SupabaseService,
tags_service: TagAggregationService,
jinja_env: Environment,
country,
cities: List,
export_path: Path,
) -> None:
"""Export country hub page."""
country_path = export_path / country.slug
country_path.mkdir(parents=True, exist_ok=True)
# Get aggregate data
total_companies = 0
city_data = []
for city in cities:
count = db.count_companies(city_id=city.id, status="content_generated")
total_companies += count
city_data.append({
"city": city,
"company_count": count,
})
# Sort cities by company count
city_data.sort(key=lambda x: x["company_count"], reverse=True)
country_tags = tags_service.get_country_tags_by_category(country.id)
# Render template
template = jinja_env.get_template("country_hub.xml")
content = template.render(
country=country,
cities=city_data,
city_count=len(cities),
company_count=total_companies,
tags=country_tags,
)
hub_file = country_path / "index.html"
hub_file.write_text(content, encoding="utf-8")
def _export_sitemaps(
db: SupabaseService,
country,
cities: List,
export_path: Path,
) -> None:
"""Export XML sitemaps."""
sitemap_path = export_path / "sitemaps"
sitemap_path.mkdir(parents=True, exist_ok=True)
base_url = "https://odoo-expertos.com"
# Company sitemap
urls = []
for city in cities:
companies = db.list_companies(
city_id=city.id,
status="content_generated",
limit=500,
)
for company in companies:
urls.append(
f"{base_url}/{country.slug}/{city.slug}/{company.slug}.html"
)
sitemap_xml = _generate_sitemap_xml(urls)
sitemap_file = sitemap_path / f"sitemap-companies-{country.code.lower()}.xml"
sitemap_file.write_text(sitemap_xml, encoding="utf-8")
# City sitemap
city_urls = [
f"{base_url}/{country.slug}/{city.slug}/"
for city in cities
]
city_sitemap = _generate_sitemap_xml(city_urls)
city_sitemap_file = sitemap_path / f"sitemap-cities-{country.code.lower()}.xml"
city_sitemap_file.write_text(city_sitemap, encoding="utf-8")
def _generate_sitemap_xml(urls: List[str]) -> str:
"""Generate sitemap XML content."""
lines = ['']
lines.append('')
for url in urls:
lines.append(" ")
lines.append(f" {url}")
lines.append(" monthly")
lines.append(" 0.7")
lines.append(" ")
lines.append("")
return "\n".join(lines)
def _export_tag_json(
db: SupabaseService,
tags_service: TagAggregationService,
country,
cities: List,
export_path: Path,
) -> None:
"""Export tag aggregations as JSON for frontend filtering."""
json_path = export_path / "tags"
json_path.mkdir(parents=True, exist_ok=True)
# City tags
cities_path = json_path / "cities"
cities_path.mkdir(parents=True, exist_ok=True)
for city in cities:
city_json = tags_service.export_city_tags_json(city.id)
json_file = cities_path / f"{city.slug}.json"
json_file.write_text(json.dumps(city_json, indent=2), encoding="utf-8")
# Country tags
country_json = tags_service.export_country_tags_json(country.id)
country_file = json_path / f"{country.slug}.json"
country_file.write_text(json.dumps(country_json, indent=2), encoding="utf-8")
# Default templates
_DEFAULT_COMPANY_TEMPLATE = """
{{ company.meta_title or company.name }}
{{ company.name }}
{% if company.description %}
{{ company.description }}
{% endif %}
{% if company.address %}
Address: {{ company.address }}
{% endif %}
{% if company.phone %}
Phone: {{ company.phone }}
{% endif %}
{% if company.website %}
Website: {{ company.website }}
{% endif %}
{% if company.email %}
Email: {{ company.email }}
{% endif %}
{% if company.rating %}
Rating: {{ company.rating }}/5 ({{ company.review_count }} reviews)
{% endif %}
{{ company.content_html | safe }}
"""
_DEFAULT_CITY_TEMPLATE = """
Odoo Partners in {{ city.name }} | {{ country.name }}
Odoo Partners in {{ city.name }}
Discover {{ company_count }} certified Odoo partners in {{ city.name }}, {{ country.name }}.
Filter by Services
{% for tag in tags.services %}
- {{ tag.name }} ({{ tag.count }})
{% endfor %}
Odoo Partners
{% for company in companies %}
{% if company.description %}
{{ company.description }}
{% endif %}
{% if company.rating %}
{{ company.rating }}/5
{% endif %}
{% endfor %}
"""
_DEFAULT_COUNTRY_TEMPLATE = """
Odoo Partners in {{ country.name }}
Odoo Partners in {{ country.name }}
Discover {{ company_count }} certified Odoo partners in {{ city_count }} cities across {{ country.name }}.
Services
{% for tag in tags.services %}
- {{ tag.name }} ({{ tag.company_count }} companies in {{ tag.city_count }} cities)
{% endfor %}
"""