new site
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
"""
|
||||
Supabase database service for Odoo Directory.
|
||||
Handles all database operations.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from supabase import create_client, Client
|
||||
|
||||
from ..config import get_settings
|
||||
from ..models.schemas import (
|
||||
Region,
|
||||
Country,
|
||||
City,
|
||||
Tag,
|
||||
Company,
|
||||
CompanyTag,
|
||||
CityTag,
|
||||
CountryTag,
|
||||
ExecutionPlan,
|
||||
PlanStep,
|
||||
TagCategory,
|
||||
StepStatus,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SupabaseService:
|
||||
"""Service for Supabase database operations."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Supabase client."""
|
||||
settings = get_settings()
|
||||
self.client: Client = create_client(
|
||||
settings.supabase_url, settings.supabase_key
|
||||
)
|
||||
|
||||
# ==================== Region Operations ====================
|
||||
|
||||
def create_region(self, region: Region) -> Region:
|
||||
"""Create a new region."""
|
||||
data = {"name": region.name, "slug": region.slug}
|
||||
result = self.client.table("regions").insert(data).execute()
|
||||
return Region(**result.data[0])
|
||||
|
||||
def get_region_by_slug(self, slug: str) -> Optional[Region]:
|
||||
"""Get region by slug."""
|
||||
result = (
|
||||
self.client.table("regions").select("*").eq("slug", slug).execute()
|
||||
)
|
||||
if result.data:
|
||||
return Region(**result.data[0])
|
||||
return None
|
||||
|
||||
def list_regions(self) -> List[Region]:
|
||||
"""List all regions."""
|
||||
result = self.client.table("regions").select("*").execute()
|
||||
return [Region(**r) for r in result.data]
|
||||
|
||||
# ==================== Country Operations ====================
|
||||
|
||||
def create_country(self, country: Country) -> Country:
|
||||
"""Create a new country."""
|
||||
data = {
|
||||
"region_id": str(country.region_id) if country.region_id else None,
|
||||
"name": country.name,
|
||||
"name_local": country.name_local,
|
||||
"code": country.code,
|
||||
"slug": country.slug,
|
||||
"language": country.language,
|
||||
}
|
||||
result = self.client.table("countries").insert(data).execute()
|
||||
return Country(**result.data[0])
|
||||
|
||||
def get_country_by_code(self, code: str) -> Optional[Country]:
|
||||
"""Get country by ISO code."""
|
||||
result = (
|
||||
self.client.table("countries")
|
||||
.select("*")
|
||||
.eq("code", code.upper())
|
||||
.execute()
|
||||
)
|
||||
if result.data:
|
||||
return Country(**result.data[0])
|
||||
return None
|
||||
|
||||
def get_country_by_slug(self, slug: str) -> Optional[Country]:
|
||||
"""Get country by slug."""
|
||||
result = (
|
||||
self.client.table("countries").select("*").eq("slug", slug).execute()
|
||||
)
|
||||
if result.data:
|
||||
return Country(**result.data[0])
|
||||
return None
|
||||
|
||||
def list_countries(self, region_id: Optional[UUID] = None) -> List[Country]:
|
||||
"""List countries, optionally filtered by region."""
|
||||
query = self.client.table("countries").select("*")
|
||||
if region_id:
|
||||
query = query.eq("region_id", str(region_id))
|
||||
result = query.execute()
|
||||
return [Country(**c) for c in result.data]
|
||||
|
||||
# ==================== City Operations ====================
|
||||
|
||||
def create_city(self, city: City) -> City:
|
||||
"""Create a new city."""
|
||||
data = {
|
||||
"country_id": str(city.country_id) if city.country_id else None,
|
||||
"name": city.name,
|
||||
"name_local": city.name_local,
|
||||
"slug": city.slug,
|
||||
"state": city.state,
|
||||
"population": city.population,
|
||||
"latitude": city.latitude,
|
||||
"longitude": city.longitude,
|
||||
}
|
||||
result = self.client.table("cities").insert(data).execute()
|
||||
return City(**result.data[0])
|
||||
|
||||
def get_city_by_slug(
|
||||
self, slug: str, country_id: Optional[UUID] = None
|
||||
) -> Optional[City]:
|
||||
"""Get city by slug, optionally filtered by country."""
|
||||
query = self.client.table("cities").select("*").eq("slug", slug)
|
||||
if country_id:
|
||||
query = query.eq("country_id", str(country_id))
|
||||
result = query.execute()
|
||||
if result.data:
|
||||
return City(**result.data[0])
|
||||
return None
|
||||
|
||||
def list_cities(self, country_id: Optional[UUID] = None) -> List[City]:
|
||||
"""List cities, optionally filtered by country."""
|
||||
query = self.client.table("cities").select("*")
|
||||
if country_id:
|
||||
query = query.eq("country_id", str(country_id))
|
||||
result = query.execute()
|
||||
return [City(**c) for c in result.data]
|
||||
|
||||
# ==================== Tag Operations ====================
|
||||
|
||||
def create_tag(self, tag: Tag) -> Tag:
|
||||
"""Create a new tag."""
|
||||
data = {
|
||||
"category": tag.category.value,
|
||||
"name": tag.name,
|
||||
"slug": tag.slug,
|
||||
"description": tag.description,
|
||||
}
|
||||
result = self.client.table("tags").insert(data).execute()
|
||||
return Tag(**result.data[0])
|
||||
|
||||
def get_tag_by_slug(self, slug: str) -> Optional[Tag]:
|
||||
"""Get tag by slug."""
|
||||
result = self.client.table("tags").select("*").eq("slug", slug).execute()
|
||||
if result.data:
|
||||
return Tag(**result.data[0])
|
||||
return None
|
||||
|
||||
def list_tags(self, category: Optional[TagCategory] = None) -> List[Tag]:
|
||||
"""List tags, optionally filtered by category."""
|
||||
query = self.client.table("tags").select("*")
|
||||
if category:
|
||||
query = query.eq("category", category.value)
|
||||
result = query.execute()
|
||||
return [Tag(**t) for t in result.data]
|
||||
|
||||
def get_or_create_tag(
|
||||
self, name: str, category: TagCategory, description: Optional[str] = None
|
||||
) -> Tag:
|
||||
"""Get existing tag or create new one."""
|
||||
slug = name.lower().replace(" ", "-").replace("&", "and")
|
||||
existing = self.get_tag_by_slug(slug)
|
||||
if existing:
|
||||
return existing
|
||||
return self.create_tag(
|
||||
Tag(category=category, name=name, slug=slug, description=description)
|
||||
)
|
||||
|
||||
# ==================== Company Operations ====================
|
||||
|
||||
def create_company(self, company: Company) -> Company:
|
||||
"""Create a new company."""
|
||||
data = {
|
||||
"city_id": str(company.city_id) if company.city_id else None,
|
||||
"name": company.name,
|
||||
"slug": company.slug,
|
||||
"google_place_id": company.google_place_id,
|
||||
"address": company.address,
|
||||
"phone": company.phone,
|
||||
"website": company.website,
|
||||
"email": company.email,
|
||||
"rating": company.rating,
|
||||
"review_count": company.review_count,
|
||||
"latitude": company.latitude,
|
||||
"longitude": company.longitude,
|
||||
"status": company.status,
|
||||
}
|
||||
result = self.client.table("companies").insert(data).execute()
|
||||
return Company(**result.data[0])
|
||||
|
||||
def update_company(self, company_id: UUID, updates: Dict[str, Any]) -> Company:
|
||||
"""Update a company."""
|
||||
result = (
|
||||
self.client.table("companies")
|
||||
.update(updates)
|
||||
.eq("id", str(company_id))
|
||||
.execute()
|
||||
)
|
||||
return Company(**result.data[0])
|
||||
|
||||
def get_company_by_id(self, company_id: UUID) -> Optional[Company]:
|
||||
"""Get company by ID."""
|
||||
result = (
|
||||
self.client.table("companies")
|
||||
.select("*")
|
||||
.eq("id", str(company_id))
|
||||
.execute()
|
||||
)
|
||||
if result.data:
|
||||
return Company(**result.data[0])
|
||||
return None
|
||||
|
||||
def get_company_by_place_id(self, place_id: str) -> Optional[Company]:
|
||||
"""Get company by Google Place ID (for deduplication)."""
|
||||
result = (
|
||||
self.client.table("companies")
|
||||
.select("*")
|
||||
.eq("google_place_id", place_id)
|
||||
.execute()
|
||||
)
|
||||
if result.data:
|
||||
return Company(**result.data[0])
|
||||
return None
|
||||
|
||||
def list_companies(
|
||||
self,
|
||||
city_id: Optional[UUID] = None,
|
||||
status: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> List[Company]:
|
||||
"""List companies with optional filters."""
|
||||
query = self.client.table("companies").select("*")
|
||||
if city_id:
|
||||
query = query.eq("city_id", str(city_id))
|
||||
if status:
|
||||
query = query.eq("status", status)
|
||||
query = query.range(offset, offset + limit - 1)
|
||||
result = query.execute()
|
||||
return [Company(**c) for c in result.data]
|
||||
|
||||
def count_companies(
|
||||
self, city_id: Optional[UUID] = None, status: Optional[str] = None
|
||||
) -> int:
|
||||
"""Count companies with optional filters."""
|
||||
query = self.client.table("companies").select("id", count="exact")
|
||||
if city_id:
|
||||
query = query.eq("city_id", str(city_id))
|
||||
if status:
|
||||
query = query.eq("status", status)
|
||||
result = query.execute()
|
||||
return result.count or 0
|
||||
|
||||
# ==================== Company Tag Operations ====================
|
||||
|
||||
def add_company_tag(
|
||||
self, company_id: UUID, tag_id: UUID, confidence: float = 1.0
|
||||
) -> CompanyTag:
|
||||
"""Add a tag to a company."""
|
||||
data = {
|
||||
"company_id": str(company_id),
|
||||
"tag_id": str(tag_id),
|
||||
"confidence": confidence,
|
||||
}
|
||||
result = self.client.table("company_tags").insert(data).execute()
|
||||
return CompanyTag(**result.data[0])
|
||||
|
||||
def get_company_tags(self, company_id: UUID) -> List[Tag]:
|
||||
"""Get all tags for a company."""
|
||||
result = (
|
||||
self.client.table("company_tags")
|
||||
.select("tag_id, tags(*)")
|
||||
.eq("company_id", str(company_id))
|
||||
.execute()
|
||||
)
|
||||
tags = []
|
||||
for item in result.data:
|
||||
if item.get("tags"):
|
||||
tags.append(Tag(**item["tags"]))
|
||||
return tags
|
||||
|
||||
def remove_company_tags(self, company_id: UUID) -> None:
|
||||
"""Remove all tags from a company."""
|
||||
self.client.table("company_tags").delete().eq(
|
||||
"company_id", str(company_id)
|
||||
).execute()
|
||||
|
||||
# ==================== City Tag Aggregation ====================
|
||||
|
||||
def upsert_city_tag(self, city_id: UUID, tag_id: UUID, count: int) -> CityTag:
|
||||
"""Upsert city tag aggregation."""
|
||||
data = {
|
||||
"city_id": str(city_id),
|
||||
"tag_id": str(tag_id),
|
||||
"company_count": count,
|
||||
}
|
||||
result = (
|
||||
self.client.table("city_tags")
|
||||
.upsert(data, on_conflict="city_id,tag_id")
|
||||
.execute()
|
||||
)
|
||||
return CityTag(**result.data[0])
|
||||
|
||||
def get_city_tags(self, city_id: UUID) -> List[Dict[str, Any]]:
|
||||
"""Get all tag aggregations for a city with tag details."""
|
||||
result = (
|
||||
self.client.table("city_tags")
|
||||
.select("*, tags(*)")
|
||||
.eq("city_id", str(city_id))
|
||||
.execute()
|
||||
)
|
||||
return result.data
|
||||
|
||||
# ==================== Country Tag Aggregation ====================
|
||||
|
||||
def upsert_country_tag(
|
||||
self, country_id: UUID, tag_id: UUID, company_count: int, city_count: int
|
||||
) -> CountryTag:
|
||||
"""Upsert country tag aggregation."""
|
||||
data = {
|
||||
"country_id": str(country_id),
|
||||
"tag_id": str(tag_id),
|
||||
"company_count": company_count,
|
||||
"city_count": city_count,
|
||||
}
|
||||
result = (
|
||||
self.client.table("country_tags")
|
||||
.upsert(data, on_conflict="country_id,tag_id")
|
||||
.execute()
|
||||
)
|
||||
return CountryTag(**result.data[0])
|
||||
|
||||
def get_country_tags(self, country_id: UUID) -> List[Dict[str, Any]]:
|
||||
"""Get all tag aggregations for a country with tag details."""
|
||||
result = (
|
||||
self.client.table("country_tags")
|
||||
.select("*, tags(*)")
|
||||
.eq("country_id", str(country_id))
|
||||
.execute()
|
||||
)
|
||||
return result.data
|
||||
|
||||
# ==================== Execution Plan Operations ====================
|
||||
|
||||
def create_execution_plan(self, plan: ExecutionPlan) -> ExecutionPlan:
|
||||
"""Create a new execution plan."""
|
||||
data = {
|
||||
"name": plan.name,
|
||||
"country_code": plan.country_code,
|
||||
"cities": plan.cities,
|
||||
"status": plan.status.value,
|
||||
}
|
||||
result = self.client.table("execution_plans").insert(data).execute()
|
||||
return ExecutionPlan(**result.data[0])
|
||||
|
||||
def update_execution_plan(
|
||||
self, plan_id: UUID, updates: Dict[str, Any]
|
||||
) -> ExecutionPlan:
|
||||
"""Update an execution plan."""
|
||||
result = (
|
||||
self.client.table("execution_plans")
|
||||
.update(updates)
|
||||
.eq("id", str(plan_id))
|
||||
.execute()
|
||||
)
|
||||
return ExecutionPlan(**result.data[0])
|
||||
|
||||
def get_execution_plan(self, plan_id: UUID) -> Optional[ExecutionPlan]:
|
||||
"""Get execution plan by ID."""
|
||||
result = (
|
||||
self.client.table("execution_plans")
|
||||
.select("*")
|
||||
.eq("id", str(plan_id))
|
||||
.execute()
|
||||
)
|
||||
if result.data:
|
||||
plan = ExecutionPlan(**result.data[0])
|
||||
# Load steps
|
||||
steps_result = (
|
||||
self.client.table("plan_steps")
|
||||
.select("*")
|
||||
.eq("plan_id", str(plan_id))
|
||||
.execute()
|
||||
)
|
||||
plan.steps = [PlanStep(**s) for s in steps_result.data]
|
||||
return plan
|
||||
return None
|
||||
|
||||
def list_execution_plans(
|
||||
self, status: Optional[StepStatus] = None
|
||||
) -> List[ExecutionPlan]:
|
||||
"""List execution plans."""
|
||||
query = self.client.table("execution_plans").select("*")
|
||||
if status:
|
||||
query = query.eq("status", status.value)
|
||||
result = query.execute()
|
||||
return [ExecutionPlan(**p) for p in result.data]
|
||||
|
||||
# ==================== Plan Step Operations ====================
|
||||
|
||||
def create_plan_step(self, step: PlanStep) -> PlanStep:
|
||||
"""Create a new plan step."""
|
||||
data = {
|
||||
"plan_id": str(step.plan_id) if step.plan_id else None,
|
||||
"step_name": step.step_name,
|
||||
"city_id": str(step.city_id) if step.city_id else None,
|
||||
"status": step.status.value,
|
||||
"items_processed": step.items_processed,
|
||||
"items_total": step.items_total,
|
||||
}
|
||||
result = self.client.table("plan_steps").insert(data).execute()
|
||||
return PlanStep(**result.data[0])
|
||||
|
||||
def update_plan_step(self, step_id: UUID, updates: Dict[str, Any]) -> PlanStep:
|
||||
"""Update a plan step."""
|
||||
result = (
|
||||
self.client.table("plan_steps")
|
||||
.update(updates)
|
||||
.eq("id", str(step_id))
|
||||
.execute()
|
||||
)
|
||||
return PlanStep(**result.data[0])
|
||||
|
||||
def get_plan_steps(
|
||||
self, plan_id: UUID, step_name: Optional[str] = None
|
||||
) -> List[PlanStep]:
|
||||
"""Get steps for a plan."""
|
||||
query = (
|
||||
self.client.table("plan_steps").select("*").eq("plan_id", str(plan_id))
|
||||
)
|
||||
if step_name:
|
||||
query = query.eq("step_name", step_name)
|
||||
result = query.execute()
|
||||
return [PlanStep(**s) for s in result.data]
|
||||
|
||||
# ==================== Error Logging ====================
|
||||
|
||||
def log_error(
|
||||
self,
|
||||
entity_type: str,
|
||||
entity_id: Optional[UUID],
|
||||
operation: str,
|
||||
error_message: str,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""Log an error to the database."""
|
||||
data = {
|
||||
"entity_type": entity_type,
|
||||
"entity_id": str(entity_id) if entity_id else None,
|
||||
"operation": operation,
|
||||
"error_message": error_message,
|
||||
"details": details or {},
|
||||
}
|
||||
try:
|
||||
self.client.table("error_logs").insert(data).execute()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to log error to database: {e}")
|
||||
Reference in New Issue
Block a user