feat: Canonical status metadata across domains and drops

This commit is contained in:
2025-12-21 17:39:47 +01:00
parent 1a63533333
commit 719f4c0724
9 changed files with 51 additions and 7 deletions

View File

@ -30,13 +30,14 @@ async def check_domain_availability(request: DomainCheckRequest):
return DomainCheckResponse( return DomainCheckResponse(
domain=result.domain, domain=result.domain,
status=result.status.value, status=result.status,
is_available=result.is_available, is_available=result.is_available,
registrar=result.registrar, registrar=result.registrar,
expiration_date=result.expiration_date, expiration_date=result.expiration_date,
creation_date=result.creation_date, creation_date=result.creation_date,
name_servers=result.name_servers, name_servers=result.name_servers,
error_message=result.error_message, error_message=result.error_message,
status_source=getattr(result, "check_method", None),
checked_at=datetime.utcnow(), checked_at=datetime.utcnow(),
) )
@ -61,13 +62,14 @@ async def check_domain_get(domain: str, quick: bool = False):
return DomainCheckResponse( return DomainCheckResponse(
domain=result.domain, domain=result.domain,
status=result.status.value, status=result.status,
is_available=result.is_available, is_available=result.is_available,
registrar=result.registrar, registrar=result.registrar,
expiration_date=result.expiration_date, expiration_date=result.expiration_date,
creation_date=result.creation_date, creation_date=result.creation_date,
name_servers=result.name_servers, name_servers=result.name_servers,
error_message=result.error_message, error_message=result.error_message,
status_source=getattr(result, "check_method", None),
checked_at=datetime.utcnow(), checked_at=datetime.utcnow(),
) )

View File

@ -175,6 +175,7 @@ async def add_domain(
expiration_date=check_result.expiration_date, expiration_date=check_result.expiration_date,
notify_on_available=domain_data.notify_on_available, notify_on_available=domain_data.notify_on_available,
last_checked=datetime.utcnow(), last_checked=datetime.utcnow(),
last_check_method=check_result.check_method,
) )
db.add(domain) db.add(domain)
await db.flush() await db.flush()
@ -277,6 +278,7 @@ async def refresh_domain(
domain.registrar = check_result.registrar domain.registrar = check_result.registrar
domain.expiration_date = _to_naive_utc(check_result.expiration_date) domain.expiration_date = _to_naive_utc(check_result.expiration_date)
domain.last_checked = datetime.utcnow() domain.last_checked = datetime.utcnow()
domain.last_check_method = check_result.check_method
# Create check record # Create check record
check = DomainCheck( check = DomainCheck(
@ -354,6 +356,7 @@ async def refresh_all_domains(
domain.registrar = check_result.registrar domain.registrar = check_result.registrar
domain.expiration_date = _to_naive_utc(check_result.expiration_date) domain.expiration_date = _to_naive_utc(check_result.expiration_date)
domain.last_checked = datetime.utcnow() domain.last_checked = datetime.utcnow()
domain.last_check_method = check_result.check_method
# Create check record # Create check record
check = DomainCheck( check = DomainCheck(

View File

@ -221,7 +221,9 @@ async def api_check_drop_status(
.values( .values(
availability_status=status_result.status, availability_status=status_result.status,
rdap_status=str(status_result.rdap_status) if status_result.rdap_status else None, rdap_status=str(status_result.rdap_status) if status_result.rdap_status else None,
last_status_check=datetime.utcnow() last_status_check=datetime.utcnow(),
deletion_date=status_result.deletion_date,
last_check_method=status_result.check_method,
) )
) )
await db.commit() await db.commit()
@ -235,6 +237,8 @@ async def api_check_drop_status(
"should_track": status_result.should_monitor, "should_track": status_result.should_monitor,
"message": status_result.message, "message": status_result.message,
"deletion_date": status_result.deletion_date.isoformat() if status_result.deletion_date else None, "deletion_date": status_result.deletion_date.isoformat() if status_result.deletion_date else None,
"status_checked_at": datetime.utcnow().isoformat(),
"status_source": status_result.check_method,
} }
except Exception as e: except Exception as e:

View File

@ -109,6 +109,11 @@ async def apply_migrations(conn: AsyncConnection) -> None:
# 2b) domains indexes (watchlist list/sort/filter) # 2b) domains indexes (watchlist list/sort/filter)
# --------------------------------------------------------- # ---------------------------------------------------------
if await _table_exists(conn, "domains"): if await _table_exists(conn, "domains"):
# Canonical status metadata (optional)
if not await _has_column(conn, "domains", "last_check_method"):
logger.info("DB migrations: adding column domains.last_check_method")
await conn.execute(text("ALTER TABLE domains ADD COLUMN last_check_method VARCHAR(30)"))
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_user_id ON domains(user_id)")) await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_user_id ON domains(user_id)"))
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_status ON domains(status)")) await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_status ON domains(status)"))
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_user_created_at ON domains(user_id, created_at)")) await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_user_created_at ON domains(user_id, created_at)"))
@ -130,6 +135,10 @@ async def apply_migrations(conn: AsyncConnection) -> None:
# 2d) dropped_domains indexes + de-duplication # 2d) dropped_domains indexes + de-duplication
# --------------------------------------------------------- # ---------------------------------------------------------
if await _table_exists(conn, "dropped_domains"): if await _table_exists(conn, "dropped_domains"):
if not await _has_column(conn, "dropped_domains", "last_check_method"):
logger.info("DB migrations: adding column dropped_domains.last_check_method")
await conn.execute(text("ALTER TABLE dropped_domains ADD COLUMN last_check_method VARCHAR(30)"))
# Query patterns: # Query patterns:
# - by time window (dropped_date) + optional tld + keyword # - by time window (dropped_date) + optional tld + keyword
# - status updates (availability_status + last_status_check) # - status updates (availability_status + last_status_check)

View File

@ -42,6 +42,8 @@ class Domain(Base):
# Timestamps # Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
last_checked: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) last_checked: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
# How the current status was derived (rdap_iana, whois, dns, etc.)
last_check_method: Mapped[str | None] = mapped_column(String(30), nullable=True)
# Check history relationship # Check history relationship
checks: Mapped[list["DomainCheck"]] = relationship( checks: Mapped[list["DomainCheck"]] = relationship(
@ -54,6 +56,17 @@ class Domain(Base):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Domain {self.name} ({self.status})>" return f"<Domain {self.name} ({self.status})>"
# ------------------------------------------------------------------
# Canonical status fields (API stability for Terminal consistency)
# ------------------------------------------------------------------
@property
def status_checked_at(self) -> datetime | None:
return self.last_checked
@property
def status_source(self) -> str | None:
return self.last_check_method
class DomainCheck(Base): class DomainCheck(Base):
"""History of domain availability checks.""" """History of domain availability checks."""

View File

@ -43,6 +43,7 @@ class DroppedDomain(Base):
rdap_status = Column(String(255), nullable=True) # Raw RDAP status string rdap_status = Column(String(255), nullable=True) # Raw RDAP status string
last_status_check = Column(DateTime, nullable=True) last_status_check = Column(DateTime, nullable=True)
deletion_date = Column(DateTime, nullable=True) # When domain will be fully deleted deletion_date = Column(DateTime, nullable=True) # When domain will be fully deleted
last_check_method = Column(String(30), nullable=True) # rdap_iana, rdap_ch, error, etc.
__table_args__ = ( __table_args__ = (
Index('ix_dropped_domains_tld_date', 'tld', 'dropped_date'), Index('ix_dropped_domains_tld_date', 'tld', 'dropped_date'),

View File

@ -170,6 +170,7 @@ async def check_domains_by_frequency(frequency: str):
domain.registrar = check_result.registrar domain.registrar = check_result.registrar
domain.expiration_date = check_result.expiration_date domain.expiration_date = check_result.expiration_date
domain.last_checked = datetime.utcnow() domain.last_checked = datetime.utcnow()
domain.last_check_method = getattr(check_result, "check_method", None)
# Create check record for history # Create check record for history
check = DomainCheck( check = DomainCheck(

View File

@ -39,9 +39,13 @@ class DomainResponse(BaseModel):
is_available: bool is_available: bool
registrar: Optional[str] registrar: Optional[str]
expiration_date: Optional[datetime] expiration_date: Optional[datetime]
deletion_date: Optional[datetime] = None
notify_on_available: bool notify_on_available: bool
created_at: datetime created_at: datetime
last_checked: Optional[datetime] last_checked: Optional[datetime]
# Canonical status metadata (stable across Terminal modules)
status_checked_at: Optional[datetime] = None
status_source: Optional[str] = None
class Config: class Config:
from_attributes = True from_attributes = True
@ -70,13 +74,14 @@ class DomainCheckRequest(BaseModel):
class DomainCheckResponse(BaseModel): class DomainCheckResponse(BaseModel):
"""Schema for domain check response.""" """Schema for domain check response."""
domain: str domain: str
status: str status: DomainStatus
is_available: bool is_available: bool
registrar: Optional[str] = None registrar: Optional[str] = None
expiration_date: Optional[datetime] = None expiration_date: Optional[datetime] = None
creation_date: Optional[datetime] = None creation_date: Optional[datetime] = None
name_servers: Optional[List[str]] = None name_servers: Optional[List[str]] = None
error_message: Optional[str] = None error_message: Optional[str] = None
status_source: Optional[str] = None
checked_at: datetime checked_at: datetime

View File

@ -396,9 +396,13 @@ async def get_dropped_domains(
"length": item.length, "length": item.length,
"is_numeric": item.is_numeric, "is_numeric": item.is_numeric,
"has_hyphen": item.has_hyphen, "has_hyphen": item.has_hyphen,
"availability_status": getattr(item, 'availability_status', 'unknown') or 'unknown', # Canonical status fields (keep old key for backwards compat)
"last_status_check": item.last_status_check.isoformat() if getattr(item, 'last_status_check', None) else None, "availability_status": getattr(item, "availability_status", "unknown") or "unknown",
"deletion_date": item.deletion_date.isoformat() if getattr(item, 'deletion_date', None) else None, "status": getattr(item, "availability_status", "unknown") or "unknown",
"last_status_check": item.last_status_check.isoformat() if getattr(item, "last_status_check", None) else None,
"status_checked_at": item.last_status_check.isoformat() if getattr(item, "last_status_check", None) else None,
"status_source": getattr(item, "last_check_method", None),
"deletion_date": item.deletion_date.isoformat() if getattr(item, "deletion_date", None) else None,
} }
for item in items for item in items
] ]
@ -578,6 +582,7 @@ async def verify_drops_availability(
"rdap_status": str(status_result.rdap_status)[:255] if status_result.rdap_status else None, "rdap_status": str(status_result.rdap_status)[:255] if status_result.rdap_status else None,
"last_status_check": now, "last_status_check": now,
"deletion_date": status_result.deletion_date, "deletion_date": status_result.deletion_date,
"last_check_method": status_result.check_method,
} }
) )
@ -590,6 +595,7 @@ async def verify_drops_availability(
rdap_status=bindparam("rdap_status"), rdap_status=bindparam("rdap_status"),
last_status_check=bindparam("last_status_check"), last_status_check=bindparam("last_status_check"),
deletion_date=bindparam("deletion_date"), deletion_date=bindparam("deletion_date"),
last_check_method=bindparam("last_check_method"),
) )
) )
await db.execute(stmt, updates) await db.execute(stmt, updates)